From 0c61aaa7ca9b4c62baf9ff285931b137b0335d20 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 13:30:53 +0100 Subject: [PATCH 1/6] feat(sidecar): enable-unsafe-lookahead flag in config --- bolt-sidecar/src/builder/signature.rs | 8 +-- bolt-sidecar/src/config/chain.rs | 97 ++++++++++++++++++--------- bolt-sidecar/src/state/consensus.rs | 4 +- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/bolt-sidecar/src/builder/signature.rs b/bolt-sidecar/src/builder/signature.rs index ee1d0e57c..aebbcecc1 100644 --- a/bolt-sidecar/src/builder/signature.rs +++ b/bolt-sidecar/src/builder/signature.rs @@ -122,25 +122,25 @@ mod tests { fn test_compute_builder_domain() { let mainnet = ChainConfig::mainnet(); assert_eq!( - compute_builder_domain(mainnet.fork_version(), None), + compute_builder_domain(mainnet.chain.fork_version(), None), mainnet.application_builder_domain() ); let holesky = ChainConfig::holesky(); assert_eq!( - compute_builder_domain(holesky.fork_version(), None), + compute_builder_domain(holesky.chain.fork_version(), None), holesky.application_builder_domain() ); let kurtosis = ChainConfig::kurtosis(0, 0); assert_eq!( - compute_builder_domain(kurtosis.fork_version(), None), + compute_builder_domain(kurtosis.chain.fork_version(), None), kurtosis.application_builder_domain() ); let helder = ChainConfig::helder(); assert_eq!( - compute_builder_domain(helder.fork_version(), None), + compute_builder_domain(helder.chain.fork_version(), None), helder.application_builder_domain() ); } diff --git a/bolt-sidecar/src/config/chain.rs b/bolt-sidecar/src/config/chain.rs index 4ce4f88a6..b8c31b465 100644 --- a/bolt-sidecar/src/config/chain.rs +++ b/bolt-sidecar/src/config/chain.rs @@ -1,4 +1,8 @@ -use std::time::Duration; +use core::fmt; +use std::{ + fmt::{Display, Formatter}, + time::Duration, +}; use clap::{Args, ValueEnum}; use ethereum_consensus::deneb::{compute_fork_data_root, Root}; @@ -20,38 +24,52 @@ pub const APPLICATION_BUILDER_DOMAIN_MASK: [u8; 4] = [0, 0, 0, 1]; /// The domain mask for signing commit-boost messages. pub const COMMIT_BOOST_DOMAIN_MASK: [u8; 4] = [109, 109, 111, 67]; +pub const DEFAULT_CHAIN_CONFIG: ChainConfig = ChainConfig { + chain: Chain::Mainnet, + commitment_deadline: DEFAULT_COMMITMENT_DEADLINE_IN_MILLIS, + slot_time: DEFAULT_SLOT_TIME_IN_SECONDS, + enable_unsafe_lookahead: false, +}; + /// Configuration for the chain the sidecar is running on. -/// This allows to customize the slot time for custom Kurtosis devnets. #[derive(Debug, Clone, Copy, Args, Deserialize)] pub struct ChainConfig { /// Chain on which the sidecar is running - #[clap(long, env = "BOLT_SIDECAR_CHAIN", default_value = "mainnet")] - chain: Chain, + #[clap( + long, + env = "BOLT_SIDECAR_CHAIN", + default_value_t = DEFAULT_CHAIN_CONFIG.chain + )] + pub(crate) chain: Chain, /// The deadline in the slot at which the sidecar will stop accepting /// new commitments for the next block (parsed as milliseconds). #[clap( long, env = "BOLT_SIDECAR_COMMITMENT_DEADLINE", - default_value_t = DEFAULT_COMMITMENT_DEADLINE_IN_MILLIS + default_value_t = DEFAULT_CHAIN_CONFIG.commitment_deadline )] - commitment_deadline: u64, + pub(crate) commitment_deadline: u64, /// The slot time duration in seconds. If provided, /// it overrides the default for the selected [Chain]. #[clap( long, env = "BOLT_SIDECAR_SLOT_TIME", - default_value_t = DEFAULT_SLOT_TIME_IN_SECONDS + default_value_t = DEFAULT_CHAIN_CONFIG.slot_time, + )] + pub(crate) slot_time: u64, + /// Toggle to enable unsafe lookahead for the sidecar. If `true`, commitments requests will be + /// validated against a two-epoch lookahead window. + #[clap( + long, + env = "BOLT_SIDECAR_ENABLE_UNSAFE_LOOKAHEAD", + default_value_t = DEFAULT_CHAIN_CONFIG.enable_unsafe_lookahead )] - slot_time: u64, + pub(crate) enable_unsafe_lookahead: bool, } impl Default for ChainConfig { fn default() -> Self { - Self { - chain: Chain::Mainnet, - commitment_deadline: DEFAULT_COMMITMENT_DEADLINE_IN_MILLIS, - slot_time: DEFAULT_SLOT_TIME_IN_SECONDS, - } + DEFAULT_CHAIN_CONFIG } } @@ -65,6 +83,33 @@ pub enum Chain { Kurtosis, } +impl Chain { + pub fn name(&self) -> &'static str { + match self { + Chain::Mainnet => "mainnet", + Chain::Holesky => "holesky", + Chain::Helder => "helder", + Chain::Kurtosis => "kurtosis", + } + } + + /// Get the fork version for the given chain. + pub fn fork_version(&self) -> [u8; 4] { + match self { + Chain::Mainnet => [0, 0, 0, 0], + Chain::Holesky => [1, 1, 112, 0], + Chain::Helder => [16, 0, 0, 0], + Chain::Kurtosis => [16, 0, 0, 56], + } + } +} + +impl Display for Chain { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + impl ChainConfig { /// Get the chain ID for the given chain. pub fn chain_id(&self) -> u64 { @@ -78,12 +123,7 @@ impl ChainConfig { /// Get the chain name for the given chain. pub fn name(&self) -> &'static str { - match self.chain { - Chain::Mainnet => "mainnet", - Chain::Holesky => "holesky", - Chain::Helder => "helder", - Chain::Kurtosis => "kurtosis", - } + self.chain.name() } /// Get the slot time for the given chain in seconds. @@ -101,16 +141,6 @@ impl ChainConfig { self.compute_domain_from_mask(COMMIT_BOOST_DOMAIN_MASK) } - /// Get the fork version for the given chain. - pub fn fork_version(&self) -> [u8; 4] { - match self.chain { - Chain::Mainnet => [0, 0, 0, 0], - Chain::Holesky => [1, 1, 112, 0], - Chain::Helder => [16, 0, 0, 0], - Chain::Kurtosis => [16, 0, 0, 56], - } - } - /// Get the commitment deadline duration for the given chain. pub fn commitment_deadline(&self) -> Duration { Duration::from_millis(self.commitment_deadline) @@ -120,7 +150,7 @@ impl ChainConfig { fn compute_domain_from_mask(&self, mask: [u8; 4]) -> [u8; 32] { let mut domain = [0; 32]; - let fork_version = self.fork_version(); + let fork_version = self.chain.fork_version(); // Note: the application builder domain specs require the genesis_validators_root // to be 0x00 for any out-of-protocol message. The commit-boost domain follows the @@ -149,7 +179,12 @@ impl ChainConfig { } pub fn kurtosis(slot_time_in_seconds: u64, commitment_deadline: u64) -> Self { - Self { chain: Chain::Kurtosis, slot_time: slot_time_in_seconds, commitment_deadline } + Self { + chain: Chain::Kurtosis, + slot_time: slot_time_in_seconds, + commitment_deadline, + ..Default::default() + } } } diff --git a/bolt-sidecar/src/state/consensus.rs b/bolt-sidecar/src/state/consensus.rs index 6b9d62a12..a24f87fde 100644 --- a/bolt-sidecar/src/state/consensus.rs +++ b/bolt-sidecar/src/state/consensus.rs @@ -107,8 +107,8 @@ impl ConsensusState { } // If the request is for the next slot, check if it's within the commitment deadline - if req.slot == self.latest_slot + 1 && - self.latest_slot_timestamp + self.commitment_deadline_duration < Instant::now() + if req.slot == self.latest_slot + 1 + && self.latest_slot_timestamp + self.commitment_deadline_duration < Instant::now() { return Err(ConsensusError::DeadlineExceeded); } From ff946a49f0e865a7b2908ea851ed7cd9860f384e Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 13:32:10 +0100 Subject: [PATCH 2/6] chore(sidecar): the '!' type is a fun one --- bolt-sidecar/bin/sidecar.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/bolt-sidecar/bin/sidecar.rs b/bolt-sidecar/bin/sidecar.rs index c7fbf60cc..07c590ed3 100644 --- a/bolt-sidecar/bin/sidecar.rs +++ b/bolt-sidecar/bin/sidecar.rs @@ -40,6 +40,4 @@ async fn main() -> Result<()> { } } } - - Ok(()) } From 2c0e977a6a70980edec6defc8d08e33544bf3aa8 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 14:27:37 +0100 Subject: [PATCH 3/6] feat(sidecar): consensus validation takes unsafe lookahead into account --- bolt-sidecar/src/driver.rs | 1 + bolt-sidecar/src/state/consensus.rs | 44 ++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index 8d2a66abc..dd36caa3b 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -176,6 +176,7 @@ impl SidecarDriver { beacon_client, opts.validator_indexes.clone(), opts.chain.commitment_deadline(), + opts.chain.enable_unsafe_lookahead, ); let (payload_requests_tx, payload_requests_rx) = mpsc::channel(16); diff --git a/bolt-sidecar/src/state/consensus.rs b/bolt-sidecar/src/state/consensus.rs index a24f87fde..4319a4c49 100644 --- a/bolt-sidecar/src/state/consensus.rs +++ b/bolt-sidecar/src/state/consensus.rs @@ -5,6 +5,7 @@ use std::{ use beacon_api_client::{mainnet::Client, ProposerDuty}; use ethereum_consensus::{crypto::PublicKey as BlsPublicKey, phase0::mainnet::SLOTS_PER_EPOCH}; +use tokio::join; use tracing::debug; use super::CommitmentDeadline; @@ -32,10 +33,15 @@ pub enum ConsensusError { /// Represents an epoch in the beacon chain. #[derive(Debug, Default)] -#[allow(missing_docs)] -pub struct Epoch { +struct Epoch { + /// The epoch number pub value: u64, + /// The start slot of the epoch pub start_slot: Slot, + /// The proposer duties of the epoch. + /// + /// NOTE: if the unsafe lookhead flag is enabled, then this field represents the proposer + /// duties also for the next epoch. pub proposer_duties: Vec, } @@ -58,6 +64,8 @@ pub struct ConsensusState { pub commitment_deadline: CommitmentDeadline, /// The duration of the commitment deadline. commitment_deadline_duration: Duration, + /// If commitment requests should be validated against also against the unsafe lookahead + pub unsafe_lookahead_enabled: bool, } impl fmt::Debug for ConsensusState { @@ -77,6 +85,7 @@ impl ConsensusState { beacon_api_client: BeaconClient, validator_indexes: ValidatorIndexes, commitment_deadline_duration: Duration, + unsafe_lookahead_enabled: bool, ) -> Self { ConsensusState { beacon_api_client, @@ -86,6 +95,7 @@ impl ConsensusState { latest_slot_timestamp: Instant::now(), commitment_deadline: CommitmentDeadline::new(0, commitment_deadline_duration), commitment_deadline_duration, + unsafe_lookahead_enabled, } } @@ -101,8 +111,12 @@ impl ConsensusState { ) -> Result { let CommitmentRequest::Inclusion(req) = request; - // Check if the slot is in the current epoch - if req.slot < self.epoch.start_slot || req.slot >= self.epoch.start_slot + SLOTS_PER_EPOCH { + let furthest_slot = self.epoch.start_slot + + SLOTS_PER_EPOCH + + if self.unsafe_lookahead_enabled { SLOTS_PER_EPOCH } else { 0 }; + + // Check if the slot is in the current epoch or next epoch (if unsafe lookahead is enabled) + if req.slot < self.epoch.start_slot || req.slot >= furthest_slot { return Err(ConsensusError::InvalidSlot(req.slot)); } @@ -151,11 +165,27 @@ impl ConsensusState { Ok(()) } - /// Fetch proposer duties for the given epoch. + /// Fetch proposer duties for the given epoch and the next one if the unsafe lookahead flag is set async fn fetch_proposer_duties(&mut self, epoch: u64) -> Result<(), ConsensusError> { - let duties = self.beacon_api_client.get_proposer_duties(epoch).await?; + let duties = if self.unsafe_lookahead_enabled { + let two_epoch_duties = join!( + self.beacon_api_client.get_proposer_duties(epoch), + self.beacon_api_client.get_proposer_duties(epoch + 1) + ); + + match two_epoch_duties { + (Ok((_, mut duties)), Ok((_, next_duties))) => { + duties.extend(next_duties); + duties + } + (Err(e), _) | (_, Err(e)) => return Err(ConsensusError::BeaconApiError(e)), + } + } else { + self.beacon_api_client.get_proposer_duties(epoch).await?.1 + }; + + self.epoch.proposer_duties = duties; - self.epoch.proposer_duties = duties.1; Ok(()) } From 6f5b3442c480a9ba27e684ff664c314abf8427ed Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 14:28:59 +0100 Subject: [PATCH 4/6] test(sidecar): fetch two-epoch proposer duties --- bolt-sidecar/src/state/consensus.rs | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/bolt-sidecar/src/state/consensus.rs b/bolt-sidecar/src/state/consensus.rs index 4319a4c49..9b4548c41 100644 --- a/bolt-sidecar/src/state/consensus.rs +++ b/bolt-sidecar/src/state/consensus.rs @@ -204,7 +204,7 @@ impl ConsensusState { #[cfg(test)] mod tests { - use beacon_api_client::ProposerDuty; + use beacon_api_client::{BlockId, ProposerDuty}; use reqwest::Url; use tracing::warn; @@ -232,6 +232,7 @@ mod tests { validator_indexes, commitment_deadline_duration: Duration::from_secs(1), latest_slot: 0, + unsafe_lookahead_enabled: false, }; // Test finding a valid slot @@ -268,6 +269,7 @@ mod tests { validator_indexes, commitment_deadline: CommitmentDeadline::new(0, commitment_deadline_duration), commitment_deadline_duration, + unsafe_lookahead_enabled: false, }; // Update the slot to 32 @@ -290,4 +292,40 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_fetch_proposer_duties() -> eyre::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let Some(url) = try_get_beacon_api_url().await else { + warn!("skipping test: beacon API URL is not reachable"); + return Ok(()); + }; + + let beacon_client = BeaconClient::new(Url::parse(url).unwrap()); + + let commitment_deadline_duration = Duration::from_secs(1); + + // Create the initial ConsensusState + let mut state = ConsensusState { + beacon_api_client: beacon_client, + epoch: Epoch::default(), + latest_slot: Default::default(), + latest_slot_timestamp: Instant::now(), + validator_indexes: Default::default(), + commitment_deadline: CommitmentDeadline::new(0, commitment_deadline_duration), + commitment_deadline_duration, + // We test for both epochs + unsafe_lookahead_enabled: true, + }; + + let epoch = + state.beacon_api_client.get_beacon_header(BlockId::Head).await?.header.message.slot + / SLOTS_PER_EPOCH; + + state.fetch_proposer_duties(epoch).await?; + assert_eq!(state.epoch.proposer_duties.len(), SLOTS_PER_EPOCH as usize * 2); + + Ok(()) + } } From 7640f2d00852cad003eef828ddc28c7b2cea24d8 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 15:18:10 +0100 Subject: [PATCH 5/6] chore(sidecar): furthest_slot helper --- bolt-sidecar/src/state/consensus.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bolt-sidecar/src/state/consensus.rs b/bolt-sidecar/src/state/consensus.rs index 9b4548c41..644454f61 100644 --- a/bolt-sidecar/src/state/consensus.rs +++ b/bolt-sidecar/src/state/consensus.rs @@ -111,12 +111,8 @@ impl ConsensusState { ) -> Result { let CommitmentRequest::Inclusion(req) = request; - let furthest_slot = self.epoch.start_slot - + SLOTS_PER_EPOCH - + if self.unsafe_lookahead_enabled { SLOTS_PER_EPOCH } else { 0 }; - // Check if the slot is in the current epoch or next epoch (if unsafe lookahead is enabled) - if req.slot < self.epoch.start_slot || req.slot >= furthest_slot { + if req.slot < self.epoch.start_slot || req.slot >= self.furthest_slot() { return Err(ConsensusError::InvalidSlot(req.slot)); } @@ -200,6 +196,14 @@ impl ConsensusState { .map(|duty| duty.public_key.clone()) .ok_or(ConsensusError::ValidatorNotFound) } + + /// Returns the furthest slot for which a commitment request is considered valid, whether in + /// the current epoch or next epoch (if unsafe lookahead is enabled) + fn furthest_slot(&self) -> u64 { + self.epoch.start_slot + + SLOTS_PER_EPOCH + + if self.unsafe_lookahead_enabled { SLOTS_PER_EPOCH } else { 0 } + } } #[cfg(test)] From 510b2983c616b7cf0e7ae4d21bf4f83749b3314a Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 28 Oct 2024 15:22:46 +0100 Subject: [PATCH 6/6] fix(sidecar): typo --- bolt-sidecar/src/state/consensus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bolt-sidecar/src/state/consensus.rs b/bolt-sidecar/src/state/consensus.rs index 644454f61..7b41c4a26 100644 --- a/bolt-sidecar/src/state/consensus.rs +++ b/bolt-sidecar/src/state/consensus.rs @@ -64,7 +64,7 @@ pub struct ConsensusState { pub commitment_deadline: CommitmentDeadline, /// The duration of the commitment deadline. commitment_deadline_duration: Duration, - /// If commitment requests should be validated against also against the unsafe lookahead + /// If commitment requests should be validated also against the unsafe lookahead pub unsafe_lookahead_enabled: bool, }