diff --git a/lez/sequencer/core/src/config.rs b/lez/sequencer/core/src/config.rs index 59002c673..7a14fcadc 100644 --- a/lez/sequencer/core/src/config.rs +++ b/lez/sequencer/core/src/config.rs @@ -146,13 +146,8 @@ const fn default_metrics_address() -> Option { Some(SequencerConfig::DEFAULT_METRICS_ADDRESS) } -/// Extra fee added to every funded Bedrock transaction. -/// -/// Covers a possible rise in gas prices before it gets mined — a stuck -/// transaction keeps retrying with the same fee, so if prices rise past this -/// it never gets mined. 10k still covers a normal price jump on a block -/// transaction; the sdk's default of 200 is too low for large transactions -/// and can leave them stuck. +/// Extra fee added to every funded Bedrock transaction, covering a gas price +/// rise before it is mined. #[must_use] pub const fn default_priority_fee() -> u64 { 10_000 diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 5bd413289..e569b1c61 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -87,6 +87,9 @@ const MAX_DISPATCHES_PER_BLOCK: usize = 16; // once that path exists, instead of a fixed genesis-only key. const GENESIS_STAKE_FUNDING_KEY: [u8; 32] = [9; 32]; +/// A number of Bedrock slots, as opposed to a [`Slot`] position. +type SlotCount = u64; + /// A founding sequencer's key, plus the ownership account attesting to its stake. type FoundingStake = ( sequencer_stake_core::SequencerKey, @@ -132,23 +135,16 @@ pub struct SequencerCore { /// store handle, so leaving them running would keep the `RocksDB` lock held /// and make the home directory unopenable by a restarting sequencer. watchers: TaskGroup, - /// When the last committee-config submission went out, or `None` if none - /// has. See [`Self::COMMITTEE_SUBMISSION_COOLDOWN`]. - last_committee_submission_at: Option, + /// Channel tip slot as of the last committee-config submission. + last_committee_submission_slot: Option, } impl SequencerCore { const CHANNEL_PROBE_RETRIES: usize = 29; const CHANNEL_PROBE_RETRY_DELAY: Duration = Duration::from_secs(2); - /// Minimum wait between committee-config submission attempts. - /// [`committee_discovery::committee_update`] re-detects the same mismatch - /// on every block until Bedrock's live state catches up; without a - /// cooldown that resubmits every block cycle, each attempt racing (and - /// likely invalidating) the last one before it has any chance to land. - /// Comfortably above the round-robin reclaim window - /// (`DEFAULT_SEQUENCER_POSTING_TIMEFRAME` + - /// `DEFAULT_SEQUENCER_POSTING_TIMEOUT` slots) plus normal confirmation lag. - const COMMITTEE_SUBMISSION_COOLDOWN: Duration = Duration::from_secs(20); + /// Channel slots between committee-config submissions; a margin over + /// observed Bedrock confirmation lag. + const COMMITTEE_SUBMISSION_COOLDOWN: SlotCount = 10; /// Starts the sequencer using the provided configuration. /// If an existing database is found, the sequencer state is loaded from it and @@ -419,7 +415,7 @@ impl SequencerCore { sequencer_config: config, block_publisher, watchers, - last_committee_submission_at: None, + last_committee_submission_slot: None, }; sequencer_core_metrics::record_chain_height(sequencer_core.chain_height()); @@ -728,6 +724,19 @@ impl SequencerCore { } } + /// Whether the channel has advanced far enough past `last_submission` to + /// submit again. A missing tip counts as no advance. + fn committee_cooldown_elapsed(last_submission: Option, tip: Option) -> bool { + let Some(last_submission) = last_submission else { + return true; + }; + tip.is_some_and(|tip| { + tip.into_inner() + .saturating_sub(last_submission.into_inner()) + >= Self::COMMITTEE_SUBMISSION_COOLDOWN + }) + } + async fn submit_committee_update( &mut self, committee_update: Option>, @@ -735,10 +744,14 @@ impl SequencerCore { let Some(new_keys) = committee_update else { return; }; - if self - .last_committee_submission_at - .is_some_and(|at| at.elapsed() < Self::COMMITTEE_SUBMISSION_COOLDOWN) - { + let tip_slot = match self.block_publisher.channel_tip_slot().await { + Ok(tip_slot) => tip_slot, + Err(err) => { + warn!("Failed to read channel tip slot; skipping committee update: {err:#}"); + return; + } + }; + if !Self::committee_cooldown_elapsed(self.last_committee_submission_slot, tip_slot) { return; } let new_keys = new_keys @@ -748,7 +761,7 @@ impl SequencerCore { .expect("sequencer key was decoded from a valid Ed25519 public key") }) .collect(); - self.last_committee_submission_at = Some(Instant::now()); + self.last_committee_submission_slot = tip_slot; if let Err(err) = self.block_publisher.submit_channel_config(new_keys).await { warn!("Failed to submit committee channel-config update: {err:#}"); } diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index 7ba401971..d5be838ca 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -21,7 +21,7 @@ use logos_blockchain_core::{ }, }; use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkPublicKey}; -use logos_blockchain_zone_sdk::sequencer::DepositInfo; +use logos_blockchain_zone_sdk::{Slot, sequencer::DepositInfo}; use mempool::MemPoolHandle; use ping_core::{ReceiverInstruction, ping_record_pda, receiver_config_account_id}; use storage::sequencer::sequencer_cells::{ @@ -135,6 +135,24 @@ fn only_the_cross_zone_inbox_is_sequencer_only() { assert!(!is_sequencer_only_program(programs::clock().id())); } +#[test] +fn committee_cooldown_needs_the_channel_to_advance() { + type Core = SequencerCoreWithMockClients; + let cooldown = Core::COMMITTEE_SUBMISSION_COOLDOWN; + let submitted_at = Slot::new(100); + + assert!(Core::committee_cooldown_elapsed(None, None)); + assert!(!Core::committee_cooldown_elapsed(Some(submitted_at), None)); + assert!(!Core::committee_cooldown_elapsed( + Some(submitted_at), + Some(Slot::new(100 + cooldown - 1)) + )); + assert!(Core::committee_cooldown_elapsed( + Some(submitted_at), + Some(Slot::new(100 + cooldown)) + )); +} + fn create_signing_key_for_account1() -> lee::PrivateKey { initial_pub_accounts_private_keys()[0].pub_sign_key.clone() }