Skip to content

Commit

Permalink
Merge pull request #330 from chainbound/lore/feat/enable-unsafe-looka…
Browse files Browse the repository at this point in the history
…head

`enable-unsafe-lookahead` flag
  • Loading branch information
thedevbirb authored Oct 28, 2024
2 parents 8b77274 + 510b298 commit 3300b32
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 47 deletions.
2 changes: 0 additions & 2 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,4 @@ async fn main() -> Result<()> {
}
}
}

Ok(())
}
8 changes: 4 additions & 4 deletions bolt-sidecar/src/builder/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}
Expand Down
97 changes: 66 additions & 31 deletions bolt-sidecar/src/config/chain.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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
}
}

Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
}
}

Expand Down
1 change: 1 addition & 0 deletions bolt-sidecar/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
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);
Expand Down
92 changes: 82 additions & 10 deletions bolt-sidecar/src/state/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ProposerDuty>,
}

Expand All @@ -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 also against the unsafe lookahead
pub unsafe_lookahead_enabled: bool,
}

impl fmt::Debug for ConsensusState {
Expand All @@ -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,
Expand All @@ -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,
}
}

Expand All @@ -101,14 +111,14 @@ impl ConsensusState {
) -> Result<BlsPublicKey, ConsensusError> {
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 {
// 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 >= self.furthest_slot() {
return Err(ConsensusError::InvalidSlot(req.slot));
}

// 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);
}
Expand Down Expand Up @@ -151,11 +161,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(())
}

Expand All @@ -170,11 +196,19 @@ 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)]
mod tests {
use beacon_api_client::ProposerDuty;
use beacon_api_client::{BlockId, ProposerDuty};
use reqwest::Url;
use tracing::warn;

Expand Down Expand Up @@ -202,6 +236,7 @@ mod tests {
validator_indexes,
commitment_deadline_duration: Duration::from_secs(1),
latest_slot: 0,
unsafe_lookahead_enabled: false,
};

// Test finding a valid slot
Expand Down Expand Up @@ -238,6 +273,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
Expand All @@ -260,4 +296,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(())
}
}

0 comments on commit 3300b32

Please sign in to comment.