Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sidecar): configurable ValidatorIndex #101

Merged
merged 14 commits into from
Jul 3, 2024
18 changes: 10 additions & 8 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async fn main() -> eyre::Result<()> {

let (api_events, mut api_events_rx) = mpsc::channel(1024);
let shutdown_tx = json_rpc::start_server(&config, api_events).await?;
let consensus_state = ConsensusState::new(&config.beacon_api_url);
let consensus_state = ConsensusState::new(&config.beacon_api_url, &config.validator_indexes);

let builder_proxy_config = BuilderProxyConfig {
mevboost_url: config.mevboost_url,
Expand Down Expand Up @@ -69,11 +69,14 @@ async fn main() -> eyre::Result<()> {
tracing::info!("Received commitment request: {:?}", event.request);
let request = event.request;

if let Err (e) = consensus_state.validate_request(&CommitmentRequest::Inclusion(request.clone())) {
tracing::error!("Failed to validate request: {:?}", e);
let _ = event.response.send(Err(ApiError::Custom(e.to_string())));
continue;
}
let validator_index = match consensus_state.validate_request(&CommitmentRequest::Inclusion(request.clone())) {
Ok(index) => index,
Err(e) => {
tracing::error!("Failed to validate request: {:?}", e);
let _ = event.response.send(Err(ApiError::Custom(e.to_string())));
continue;
}
};

if let Err(e) = execution_state
.try_commit(&CommitmentRequest::Inclusion(request.clone()))
Expand All @@ -90,8 +93,7 @@ async fn main() -> eyre::Result<()> {
);

// parse the request into constraints and sign them with the sidecar signer
// TODO: get the validator index from somewhere
let message = ConstraintsMessage::build(0, request.slot, request.clone());
let message = ConstraintsMessage::build(validator_index, request.slot, request.clone());

let signature = signer.sign(&message.digest())?;
let signed_constraints: BatchedSignedConstraints =
Expand Down
10 changes: 10 additions & 0 deletions bolt-sidecar/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::{SystemTime, UNIX_EPOCH};

use alloy_consensus::TxEnvelope;
use alloy_primitives::U256;

Expand Down Expand Up @@ -66,6 +68,14 @@ pub fn validate_transaction(
Ok(())
}

/// Get the current timestamp.
pub fn unix_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
8 changes: 8 additions & 0 deletions bolt-sidecar/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub struct Opts {
/// Max commitments to accept per block
#[clap(short = 'm', long)]
pub(super) max_commitments: Option<usize>,
/// Validator indexes
#[clap(short = 'v', long, value_parser, num_args = 1.., value_delimiter = ',')]
pub(super) validator_indexes: Vec<u64>,
/// Signing options
#[clap(flatten)]
pub(super) signing: SigningOpts,
Expand Down Expand Up @@ -68,6 +71,8 @@ pub struct Config {
pub mevboost_proxy_port: u16,
/// Limits for the sidecar
pub limits: Limits,
/// Validator indexes
pub validator_indexes: Vec<u64>,
}

impl Default for Config {
Expand All @@ -82,6 +87,7 @@ impl Default for Config {
private_key: Some(random_bls_secret()),
mevboost_proxy_port: 18551,
limits: Limits::default(),
validator_indexes: Vec::new(),
}
}
}
Expand Down Expand Up @@ -127,6 +133,8 @@ impl TryFrom<Opts> for Config {
config.beacon_api_url = opts.beacon_api_url.trim_end_matches('/').to_string();
config.mevboost_url = opts.mevboost_url.trim_end_matches('/').to_string();

config.validator_indexes = opts.validator_indexes;

Ok(config)
}
}
Expand Down
107 changes: 89 additions & 18 deletions bolt-sidecar/src/state/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
#![allow(missing_debug_implementations)]

use beacon_api_client::{mainnet::Client, BlockId, ProposerDuty};
use ethereum_consensus::deneb::BeaconBlockHeader;
use ethereum_consensus::{deneb::BeaconBlockHeader, phase0::mainnet::SLOTS_PER_EPOCH};
use reqwest::Url;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::primitives::{ChainHead, CommitmentRequest, Slot};
use crate::{
common::unix_seconds,
primitives::{ChainHead, CommitmentRequest, Slot},
};

// The slot inclusion deadline in seconds
const INCLUSION_DEADLINE: u64 = 6;
Expand All @@ -20,6 +22,8 @@ pub enum ConsensusError {
InvalidSlot(Slot),
#[error("Inclusion deadline exceeded")]
DeadlineExceeded,
#[error("Validator not found in the slot")]
ValidatorNotFound,
}

pub struct Epoch {
Expand All @@ -34,11 +38,12 @@ pub struct ConsensusState {
epoch: Epoch,
// Timestamp when the current slot is received
timestamp: u64,
validator_indexes: Vec<u64>,
}

impl ConsensusState {
/// Create a new `ConsensusState` with the given beacon client HTTP URL.
pub fn new(url: &str) -> Self {
pub fn new(url: &str, validator_indexes: &[u64]) -> Self {
let url = Url::parse(url).expect("valid beacon client URL");
let beacon_api_client = Client::new(url);

Expand All @@ -50,7 +55,8 @@ impl ConsensusState {
start_slot: 0,
proposer_duties: vec![],
},
timestamp: 0,
timestamp: unix_seconds(),
validator_indexes: validator_indexes.to_vec(),
}
}

Expand All @@ -59,20 +65,23 @@ impl ConsensusState {
/// 2. The request hasn't passed the slot deadline.
///
/// TODO: Integrate with the registry to check if we are registered.
pub fn validate_request(&self, request: &CommitmentRequest) -> Result<(), ConsensusError> {
pub fn validate_request(&self, request: &CommitmentRequest) -> Result<u64, 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 + 32 {
if req.slot < self.epoch.start_slot || req.slot >= self.epoch.start_slot + SLOTS_PER_EPOCH {
return Err(ConsensusError::InvalidSlot(req.slot));
}

// Check if the request is within the slot inclusion deadline
if self.timestamp + INCLUSION_DEADLINE < current_timestamp() {
if self.timestamp + INCLUSION_DEADLINE < unix_seconds() {
return Err(ConsensusError::DeadlineExceeded);
}

Ok(())
// Find the validator index for the given slot
let validator_index = self.find_validator_index_for_slot(req.slot)?;

Ok(validator_index)
}

/// Update the latest head and fetch the relevant data from the beacon chain.
Expand All @@ -85,16 +94,16 @@ impl ConsensusState {
self.header = update.header.message;

// Update the timestamp with current time
self.timestamp = current_timestamp();
self.timestamp = unix_seconds();

// Get the current value of slot and epoch
let slot = self.header.slot;
let epoch = slot / 32;
let epoch = slot / SLOTS_PER_EPOCH;

// If the epoch has changed, update the proposer duties
if epoch != self.epoch.value {
self.epoch.value = epoch;
self.epoch.start_slot = epoch * 32;
self.epoch.start_slot = epoch * SLOTS_PER_EPOCH;

self.fetch_proposer_duties(epoch).await?;
}
Expand All @@ -109,12 +118,74 @@ impl ConsensusState {
self.epoch.proposer_duties = duties.1;
Ok(())
}

/// Filters the proposer duties and returns the validator index for a given slot
/// if it doesn't exists then returns error.
fn find_validator_index_for_slot(&self, slot: u64) -> Result<u64, ConsensusError> {
self.epoch
.proposer_duties
.iter()
.find(|&duty| {
duty.slot == slot
&& self
.validator_indexes
.contains(&(duty.validator_index as u64))
})
.map(|duty| duty.validator_index as u64)
.ok_or(ConsensusError::ValidatorNotFound)
}
}

/// Get the current timestamp.
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
#[cfg(test)]
mod tests {
use super::*;
use beacon_api_client::ProposerDuty;

#[tokio::test]
async fn test_find_validator_index_for_slot() {
// Sample proposer duties
let proposer_duties = vec![
ProposerDuty {
public_key: Default::default(),
slot: 1,
validator_index: 100,
},
ProposerDuty {
public_key: Default::default(),
slot: 2,
validator_index: 101,
},
ProposerDuty {
public_key: Default::default(),
slot: 3,
validator_index: 102,
},
];

// Validator indexes that we are interested in
let validator_indexes = vec![100, 102];

// Create a ConsensusState with the sample proposer duties and validator indexes
let state = ConsensusState {
beacon_api_client: Client::new(Url::parse("http://localhost").unwrap()),
header: BeaconBlockHeader::default(),
epoch: Epoch {
value: 0,
start_slot: 0,
proposer_duties,
},
timestamp: unix_seconds(),
validator_indexes,
};

// Test finding a valid slot
assert_eq!(state.find_validator_index_for_slot(1).unwrap(), 100);
assert_eq!(state.find_validator_index_for_slot(3).unwrap(), 102);

// Test finding an invalid slot (not in proposer duties)
assert!(matches!(
state.find_validator_index_for_slot(4),
Err(ConsensusError::ValidatorNotFound)
));
}
}
5 changes: 0 additions & 5 deletions bolt-sidecar/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ pub enum StateError {
Rpc(#[from] TransportError),
}

#[derive(Debug, Clone)]
struct ProposerDuties {
assigned_slots: Vec<u64>,
}

#[cfg(test)]
mod tests {
use alloy_consensus::constants::ETH_TO_WEI;
Expand Down