mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-26 20:01:16 +00:00
fix(lez/sequencer): use slot to track finalized config ops
This commit is contained in:
@@ -155,8 +155,9 @@ pub trait LocalBlockPublisherTrait: Sized + Sync {
|
||||
) -> Result<PublishOutcome>;
|
||||
|
||||
/// Live (adopted, possibly not yet finalized) accredited-key snapshot for
|
||||
/// this channel, read directly from the connected Bedrock node.
|
||||
async fn accredited_keys(&self) -> Result<Vec<Ed25519PublicKey>>;
|
||||
/// this channel with the slot its tip sits at, from one read of the
|
||||
/// connected Bedrock node. `None` if the channel does not exist.
|
||||
async fn accredited_keys(&self) -> Result<Option<(Vec<Ed25519PublicKey>, Slot)>>;
|
||||
|
||||
/// Submit a committee `ChannelConfigOp` as its own, independent Mantle
|
||||
/// tx (not bundled with any block publish). `new_keys` is the full
|
||||
@@ -551,14 +552,13 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accredited_keys(&self) -> Result<Vec<Ed25519PublicKey>> {
|
||||
async fn accredited_keys(&self) -> Result<Option<(Vec<Ed25519PublicKey>, Slot)>> {
|
||||
Ok(self
|
||||
.node
|
||||
.channel_state(self.channel_id)
|
||||
.await
|
||||
.context("Failed to read channel state")?
|
||||
.map(|state| state.accredited_keys.to_vec())
|
||||
.unwrap_or_default())
|
||||
.map(|state| (state.accredited_keys.to_vec(), state.tip_slot)))
|
||||
}
|
||||
|
||||
async fn submit_channel_config(&self, new_keys: Vec<Ed25519PublicKey>) -> Result<()> {
|
||||
|
||||
@@ -5,36 +5,40 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use log::warn;
|
||||
use sequencer_stake_core::{PendingUnstake, SequencerKey, SequencerStakeConfig, StakeRecord};
|
||||
|
||||
/// When each leaving key was first seen gone from the live committee.
|
||||
/// The channel slot each leaving key was first seen gone from the live committee at.
|
||||
///
|
||||
/// In memory only: a restart re-stamps at the current tip and costs a pending
|
||||
/// exit one more finality delay.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CommitteeAbsence {
|
||||
absent_since: BTreeMap<SequencerKey, u64>,
|
||||
}
|
||||
|
||||
impl CommitteeAbsence {
|
||||
/// Records one look at the live committee, taken at block height `height`.
|
||||
/// Records one look at the live committee, taken with the channel tip at
|
||||
/// `tip_slot`.
|
||||
pub fn observe(
|
||||
&mut self,
|
||||
live: &[SequencerKey],
|
||||
leaving: &BTreeSet<SequencerKey>,
|
||||
height: u64,
|
||||
tip_slot: u64,
|
||||
) {
|
||||
self.absent_since.retain(|key, _| leaving.contains(key));
|
||||
for key in leaving {
|
||||
if live.contains(key) {
|
||||
self.absent_since.remove(key);
|
||||
} else {
|
||||
self.absent_since.entry(*key).or_insert(height);
|
||||
self.absent_since.entry(*key).or_insert(tip_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a block built after `key` was seen leaving has finalized.
|
||||
/// Whether the channel slot that first showed `key` gone is irreversible.
|
||||
#[must_use]
|
||||
pub fn removal_is_final(&self, key: SequencerKey, finalized_height: Option<u64>) -> bool {
|
||||
pub fn removal_is_final(&self, key: SequencerKey, finalized_slot: Option<u64>) -> bool {
|
||||
self.absent_since
|
||||
.get(&key)
|
||||
.is_some_and(|absent_since| finalized_height > Some(*absent_since))
|
||||
.is_some_and(|absent_since| finalized_slot > Some(*absent_since))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +117,7 @@ pub fn finalize_unstake_is_valid(
|
||||
state: &lee::V03State,
|
||||
ownership_id: lee::AccountId,
|
||||
absence: &CommitteeAbsence,
|
||||
finalized_height: Option<u64>,
|
||||
finalized_slot: Option<u64>,
|
||||
) -> bool {
|
||||
let Some(record) = stake_record(state, ownership_id) else {
|
||||
return true;
|
||||
@@ -128,7 +132,7 @@ pub fn finalize_unstake_is_valid(
|
||||
};
|
||||
|
||||
let fully_drains = entry.net_stake() == 0;
|
||||
!fully_drains || absence.removal_is_final(record.sequencer_key, finalized_height)
|
||||
!fully_drains || absence.removal_is_final(record.sequencer_key, finalized_slot)
|
||||
}
|
||||
|
||||
/// Keys whose pending release takes them out of the committee.
|
||||
|
||||
@@ -687,15 +687,15 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
pub async fn update_committee_absence(
|
||||
&mut self,
|
||||
) -> Option<Vec<sequencer_stake_core::SequencerKey>> {
|
||||
let live_accredited_keys = self.live_accredited_sequencer_keys().await;
|
||||
if let Some(live) = live_accredited_keys.as_deref() {
|
||||
let snapshot = self.live_accredited_sequencer_keys().await;
|
||||
if let Some((live, tip_slot)) = snapshot.as_ref() {
|
||||
let leaving = self
|
||||
.with_state(committee_discovery::keys_leaving_the_committee)
|
||||
.await;
|
||||
let height = self.chain_height().await;
|
||||
self.committee_absence.observe(live, &leaving, height);
|
||||
self.committee_absence
|
||||
.observe(live, &leaving, tip_slot.into_inner());
|
||||
}
|
||||
live_accredited_keys
|
||||
snapshot.map(|(keys, _)| keys)
|
||||
}
|
||||
|
||||
/// Runs everything this sequencer owes its turn: builds a block from
|
||||
@@ -747,12 +747,13 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
}
|
||||
|
||||
/// Live committee snapshot for gating `FinalizeUnstake` inclusion and
|
||||
/// committee updates. `None` if it could not be read.
|
||||
/// committee updates, with the channel tip slot it was read at. `None` if
|
||||
/// the channel is missing or unreadable.
|
||||
async fn live_accredited_sequencer_keys(
|
||||
&self,
|
||||
) -> Option<Vec<sequencer_stake_core::SequencerKey>> {
|
||||
) -> Option<(Vec<sequencer_stake_core::SequencerKey>, Slot)> {
|
||||
match self.block_publisher.accredited_keys().await {
|
||||
Ok(keys) => Some(
|
||||
Ok(Some((keys, tip_slot))) => Some((
|
||||
keys.iter()
|
||||
.filter_map(|key| {
|
||||
sequencer_stake_core::SequencerKey::new(key.to_bytes()).or_else(|| {
|
||||
@@ -764,7 +765,15 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
tip_slot,
|
||||
)),
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
"No channel to read a live committee from; skipping FinalizeUnstake inclusion \
|
||||
and committee updates this round"
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to read live committee snapshot; skipping FinalizeUnstake inclusion \
|
||||
@@ -997,7 +1006,6 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
mut working_state,
|
||||
pending_dispatches,
|
||||
finalize_unstake_txs,
|
||||
finalized_height,
|
||||
committee_update,
|
||||
) = {
|
||||
let chain = self.chain.lock().await;
|
||||
@@ -1038,11 +1046,20 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
chain.head_state().clone(),
|
||||
pending,
|
||||
build_finalize_unstake_txs(chain.head_state()),
|
||||
chain.final_tip().map(|final_tip| final_tip.block_id),
|
||||
committee_update,
|
||||
)
|
||||
};
|
||||
|
||||
// The last irreversible bedrock slot.
|
||||
let finalized_slot = self
|
||||
.store
|
||||
.get_zone_checkpoint()
|
||||
.await
|
||||
.inspect_err(|err| warn!("Failed to read the zone checkpoint: {err:#}"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|checkpoint| checkpoint.lib_slot.into_inner());
|
||||
|
||||
if !settled.is_empty() {
|
||||
if let Err(err) = self
|
||||
.store
|
||||
@@ -1185,7 +1202,7 @@ impl<BP: BlockPublisherTrait, S: StorageActorTrait> SequencerCore<BP, S> {
|
||||
&working_state,
|
||||
&tx,
|
||||
&self.committee_absence,
|
||||
finalized_height,
|
||||
finalized_slot,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -2152,7 +2169,7 @@ fn finalize_unstake_is_includable(
|
||||
state: &lee::V03State,
|
||||
tx: &LeeTransaction,
|
||||
committee_absence: &committee_discovery::CommitteeAbsence,
|
||||
finalized_height: Option<u64>,
|
||||
finalized_slot: Option<u64>,
|
||||
) -> bool {
|
||||
let Some(ownership_id) = finalize_unstake_ownership_account(tx) else {
|
||||
return true;
|
||||
@@ -2161,7 +2178,7 @@ fn finalize_unstake_is_includable(
|
||||
state,
|
||||
ownership_id,
|
||||
committee_absence,
|
||||
finalized_height,
|
||||
finalized_slot,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
self.publish_block(block, Vec::new()).await
|
||||
}
|
||||
|
||||
async fn accredited_keys(&self) -> Result<Vec<Ed25519PublicKey>> {
|
||||
Ok(Vec::new())
|
||||
async fn accredited_keys(&self) -> Result<Option<(Vec<Ed25519PublicKey>, Slot)>> {
|
||||
Ok(self.tip_slot.map(|slot| (Vec::new(), slot)))
|
||||
}
|
||||
|
||||
async fn submit_channel_config(&self, _new_keys: Vec<Ed25519PublicKey>) -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user