diff --git a/bolt-sidecar/Cargo.lock b/bolt-sidecar/Cargo.lock index e3b816772..3c0f5a3e4 100644 --- a/bolt-sidecar/Cargo.lock +++ b/bolt-sidecar/Cargo.lock @@ -1678,6 +1678,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "toml 0.5.11", "tower-http", "tracing", "tracing-subscriber", diff --git a/bolt-sidecar/Cargo.toml b/bolt-sidecar/Cargo.toml index 0aa4535c7..e4f82c5cf 100644 --- a/bolt-sidecar/Cargo.toml +++ b/bolt-sidecar/Cargo.toml @@ -61,7 +61,7 @@ thiserror = "1.0" rand = "0.8.5" dotenvy = "0.15.7" regex = "1.10.5" -# backtrace = "0.3.74" +toml = "0.5" # tracing tracing = "0.1.40" diff --git a/bolt-sidecar/bin/sidecar.rs b/bolt-sidecar/bin/sidecar.rs index 909d4e402..53d687ad6 100644 --- a/bolt-sidecar/bin/sidecar.rs +++ b/bolt-sidecar/bin/sidecar.rs @@ -1,15 +1,18 @@ -use bolt_sidecar::{telemetry::init_telemetry_stack, Opts, SidecarDriver}; use clap::Parser; use eyre::{bail, Result}; use tracing::info; +use bolt_sidecar::{telemetry::init_telemetry_stack, Opts, SidecarDriver}; + #[tokio::main] async fn main() -> Result<()> { - let opts = Opts::parse(); + let opts = if let Ok(config_path) = std::env::var("BOLT_SIDECAR_CONFIG_PATH") { + Opts::parse_from_toml(config_path.as_str())? + } else { + Opts::parse() + }; - let metrics_port = - if !opts.telemetry.disable_metrics { Some(opts.telemetry.metrics_port) } else { None }; - if let Err(err) = init_telemetry_stack(metrics_port) { + if let Err(err) = init_telemetry_stack(opts.telemetry.metrics_port()) { bail!("Failed to initialize telemetry stack: {:?}", err) } diff --git a/bolt-sidecar/src/api/spec.rs b/bolt-sidecar/src/api/spec.rs index 64cd0a035..7e4be0495 100644 --- a/bolt-sidecar/src/api/spec.rs +++ b/bolt-sidecar/src/api/spec.rs @@ -172,8 +172,8 @@ pub trait ConstraintsApi: BuilderApi { ) -> Result, BuilderApiError>; /// Implements: - async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError>; + async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError>; /// Implements: - async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError>; + async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError>; } diff --git a/bolt-sidecar/src/client/constraints_client.rs b/bolt-sidecar/src/client/constraints_client.rs index 67b49866d..db0375c1a 100644 --- a/bolt-sidecar/src/client/constraints_client.rs +++ b/bolt-sidecar/src/client/constraints_client.rs @@ -30,6 +30,7 @@ use crate::{ pub struct ConstraintsClient { url: Url, client: reqwest::Client, + delegations: Vec, } impl ConstraintsClient { @@ -38,9 +39,15 @@ impl ConstraintsClient { Self { url: url.into(), client: reqwest::ClientBuilder::new().user_agent("bolt-sidecar").build().unwrap(), + delegations: Vec::new(), } } + /// Adds a list of delegations to the client. + pub fn add_delegations(&mut self, delegations: Vec) { + self.delegations.extend(delegations); + } + fn endpoint(&self, path: &str) -> Url { self.url.join(path).unwrap_or_else(|e| { error!(err = ?e, "Failed to join path: {} with url: {}", path, self.url); @@ -80,6 +87,13 @@ impl BuilderApi for ConstraintsClient { return Err(BuilderApiError::FailedRegisteringValidators(error)); } + // If there are any delegations, propagate them to the relay + if self.delegations.is_empty() { + return Ok(()); + } else if let Err(err) = self.delegate(&self.delegations).await { + error!(?err, "Failed to propagate delegations during validator registration"); + } + Ok(()) } @@ -190,12 +204,12 @@ impl ConstraintsApi for ConstraintsClient { Ok(header) } - async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError> { + async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError> { let response = self .client .post(self.endpoint(DELEGATE_PATH)) .header("content-type", "application/json") - .body(serde_json::to_string(&signed_data)?) + .body(serde_json::to_string(signed_data)?) .send() .await?; @@ -207,12 +221,12 @@ impl ConstraintsApi for ConstraintsClient { Ok(()) } - async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError> { + async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError> { let response = self .client .post(self.endpoint(REVOKE_PATH)) .header("content-type", "application/json") - .body(serde_json::to_string(&signed_data)?) + .body(serde_json::to_string(signed_data)?) .send() .await?; diff --git a/bolt-sidecar/src/client/test_util/mod.rs b/bolt-sidecar/src/client/test_util/mod.rs index 0806e45ef..0f6639bcc 100644 --- a/bolt-sidecar/src/client/test_util/mod.rs +++ b/bolt-sidecar/src/client/test_util/mod.rs @@ -98,13 +98,13 @@ impl ConstraintsApi for MockConstraintsClient { Ok(bid) } - async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError> { + async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError> { Err(BuilderApiError::Generic( "MockConstraintsClient does not support delegating".to_string(), )) } - async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError> { + async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError> { Err(BuilderApiError::Generic("MockConstraintsClient does not support revoking".to_string())) } } diff --git a/bolt-sidecar/src/common.rs b/bolt-sidecar/src/common.rs index b6366792c..b8d6e80c4 100644 --- a/bolt-sidecar/src/common.rs +++ b/bolt-sidecar/src/common.rs @@ -9,6 +9,7 @@ use alloy::primitives::U256; use blst::min_pk::SecretKey; use rand::{Rng, RngCore}; use reth_primitives::PooledTransactionsElement; +use serde::{Deserialize, Deserializer}; use crate::{ primitives::{AccountState, TransactionExt}, @@ -107,6 +108,16 @@ impl BlsSecretKeyWrapper { } } +impl<'de> Deserialize<'de> for BlsSecretKeyWrapper { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let sk = String::deserialize(deserializer)?; + Ok(BlsSecretKeyWrapper::from(sk.as_str())) + } +} + impl From<&str> for BlsSecretKeyWrapper { fn from(sk: &str) -> Self { let hex_sk = sk.strip_prefix("0x").unwrap_or(sk); @@ -158,6 +169,16 @@ impl From<&str> for JwtSecretConfig { } } +impl<'de> Deserialize<'de> for JwtSecretConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let jwt = String::deserialize(deserializer)?; + Ok(Self::from(jwt.as_str())) + } +} + impl Deref for JwtSecretConfig { type Target = str; fn deref(&self) -> &Self::Target { diff --git a/bolt-sidecar/src/config/chain.rs b/bolt-sidecar/src/config/chain.rs index f5c4769b5..4ce4f88a6 100644 --- a/bolt-sidecar/src/config/chain.rs +++ b/bolt-sidecar/src/config/chain.rs @@ -2,6 +2,7 @@ use std::time::Duration; use clap::{Args, ValueEnum}; use ethereum_consensus::deneb::{compute_fork_data_root, Root}; +use serde::Deserialize; /// Default commitment deadline duration. /// @@ -21,7 +22,7 @@ pub const COMMIT_BOOST_DOMAIN_MASK: [u8; 4] = [109, 109, 111, 67]; /// 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)] +#[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")] @@ -55,9 +56,8 @@ impl Default for ChainConfig { } /// Supported chains for the sidecar -#[derive(Debug, Clone, Copy, ValueEnum)] +#[derive(Debug, Clone, Copy, ValueEnum, Deserialize)] #[clap(rename_all = "kebab_case")] -#[allow(missing_docs)] pub enum Chain { Mainnet, Holesky, diff --git a/bolt-sidecar/src/config/limits.rs b/bolt-sidecar/src/config/limits.rs index 6bf02c47a..6bff7cbbe 100644 --- a/bolt-sidecar/src/config/limits.rs +++ b/bolt-sidecar/src/config/limits.rs @@ -1,25 +1,36 @@ -use clap::Parser; use std::num::NonZero; +use clap::Parser; +use serde::Deserialize; + // Default limit values pub const DEFAULT_MAX_COMMITMENTS: usize = 128; pub const DEFAULT_MAX_COMMITTED_GAS: u64 = 10_000_000; pub const DEFAULT_MIN_PRIORITY_FEE: u128 = 1_000_000_000; // 1 Gwei /// Limits for the sidecar. -#[derive(Debug, Parser, Clone, Copy)] +#[derive(Debug, Parser, Clone, Copy, Deserialize)] pub struct LimitsOpts { /// Max number of commitments to accept per block - #[clap(long, env = "BOLT_SIDECAR_MAX_COMMITMENTS", - default_value_t = LimitsOpts::default().max_commitments_per_slot)] + #[clap( + long, + env = "BOLT_SIDECAR_MAX_COMMITMENTS", + default_value_t = LimitsOpts::default().max_commitments_per_slot + )] pub max_commitments_per_slot: NonZero, /// Max committed gas per slot - #[clap(long, env = "BOLT_SIDECAR_MAX_COMMITTED_GAS", - default_value_t = LimitsOpts::default().max_committed_gas_per_slot)] + #[clap( + long, + env = "BOLT_SIDECAR_MAX_COMMITTED_GAS", + default_value_t = LimitsOpts::default().max_committed_gas_per_slot + )] pub max_committed_gas_per_slot: NonZero, /// Min priority fee to accept for a commitment - #[clap(long, env = "BOLT_SIDECAR_MIN_PRIORITY_FEE", - default_value_t = LimitsOpts::default().min_priority_fee)] + #[clap( + long, + env = "BOLT_SIDECAR_MIN_PRIORITY_FEE", + default_value_t = LimitsOpts::default().min_priority_fee + )] pub min_priority_fee: NonZero, } diff --git a/bolt-sidecar/src/config/mod.rs b/bolt-sidecar/src/config/mod.rs index 7110d7aca..f65d60390 100644 --- a/bolt-sidecar/src/config/mod.rs +++ b/bolt-sidecar/src/config/mod.rs @@ -1,6 +1,10 @@ +use std::{fs::File, io::Read}; + use alloy::primitives::Address; use clap::Parser; +use eyre::Context; use reqwest::Url; +use serde::Deserialize; pub mod validator_indexes; pub use validator_indexes::ValidatorIndexes; @@ -26,7 +30,7 @@ pub const DEFAULT_RPC_PORT: u16 = 8000; pub const DEFAULT_CONSTRAINTS_PROXY_PORT: u16 = 18551; /// Command-line options for the Bolt sidecar -#[derive(Debug, Parser)] +#[derive(Debug, Parser, Deserialize)] #[clap(trailing_var_arg = true)] pub struct Opts { /// Port to listen on for incoming JSON-RPC requests @@ -46,8 +50,7 @@ pub struct Opts { #[clap(long, env = "BOLT_SIDECAR_ENGINE_API_URL", default_value = "http://localhost:8551")] pub engine_api_url: Url, /// Constraint proxy server port to use - #[clap(long, env = "BOLT_SIDECAR_CONSTRAINTS_PROXY_PORT", - default_value_t = DEFAULT_CONSTRAINTS_PROXY_PORT)] + #[clap(long, env = "BOLT_SIDECAR_CONSTRAINTS_PROXY_PORT", default_value_t = DEFAULT_CONSTRAINTS_PROXY_PORT)] pub constraints_proxy_port: u16, /// Validator indexes of connected validators that the sidecar /// should accept commitments on behalf of. Accepted values: @@ -66,10 +69,8 @@ pub struct Opts { #[clap(long, env = "BOLT_SIDECAR_FEE_RECIPIENT", default_value_t = Address::ZERO)] pub fee_recipient: Address, - /// Secret BLS key to sign fallback payloads with - /// (If not provided, a random key will be used) - #[clap(long, env = "BOLT_SIDECAR_BUILDER_PRIVATE_KEY", - default_value_t = BlsSecretKeyWrapper::random())] + /// Secret BLS key to sign fallback payloads with (If not provided, a random key will be used) + #[clap(long, env = "BOLT_SIDECAR_BUILDER_PRIVATE_KEY", default_value_t = BlsSecretKeyWrapper::random())] pub builder_private_key: BlsSecretKeyWrapper, /// Operating limits for the sidecar #[clap(flatten)] @@ -90,6 +91,18 @@ pub struct Opts { pub extra_args: Vec, } +impl Opts { + /// Parse the configuration from a TOML file. + pub fn parse_from_toml(file_path: &str) -> eyre::Result { + let mut file = File::open(file_path).wrap_err("Unable to open file")?; + + let mut contents = String::new(); + file.read_to_string(&mut contents).wrap_err("Unable to read file")?; + + toml::from_str(&contents).wrap_err("Error parsing the TOML file") + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/bolt-sidecar/src/config/signing.rs b/bolt-sidecar/src/config/signing.rs index d4d91494d..421d7fbb9 100644 --- a/bolt-sidecar/src/config/signing.rs +++ b/bolt-sidecar/src/config/signing.rs @@ -1,17 +1,16 @@ -use crate::common::{BlsSecretKeyWrapper, JwtSecretConfig}; -use std::{ - fmt::{self}, - net::SocketAddr, -}; +use std::{fmt, net::SocketAddr}; use clap::{ArgGroup, Args}; use lighthouse_account_utils::ZeroizeString; +use serde::Deserialize; + +use crate::common::{BlsSecretKeyWrapper, JwtSecretConfig}; /// Command-line options for signing -#[derive(Args)] +#[derive(Args, Deserialize)] #[clap( group = ArgGroup::new("signing-opts").required(true) - .args(&["private_key", "commit_boost_address", "commit_boost_jwt_hex", "keystore_password"]) + .args(&["private_key", "commit_boost_address", "keystore_password"]) )] pub struct SigningOpts { /// Private key to use for signing preconfirmation requests @@ -30,6 +29,9 @@ pub struct SigningOpts { /// Path to the keystores folder. If not provided, the default path is used. #[clap(long, env = "BOLT_SIDECAR_KEYSTORE_PATH", requires("keystore_password"))] pub keystore_path: Option, + /// Path to the delegations file. If not provided, the default path is used. + #[clap(long, env = "BOLT_SIDECAR_DELEGATIONS_PATH")] + pub delegations_path: Option, } // Implement Debug manually to hide the keystore_password field @@ -43,15 +45,3 @@ impl fmt::Debug for SigningOpts { .finish() } } - -impl Default for SigningOpts { - fn default() -> Self { - Self { - private_key: Some(BlsSecretKeyWrapper::random()), - commit_boost_address: None, - commit_boost_jwt_hex: None, - keystore_password: None, - keystore_path: None, - } - } -} diff --git a/bolt-sidecar/src/config/telemetry.rs b/bolt-sidecar/src/config/telemetry.rs index 4e49cfaaf..33a2f3ac8 100644 --- a/bolt-sidecar/src/config/telemetry.rs +++ b/bolt-sidecar/src/config/telemetry.rs @@ -1,10 +1,22 @@ use clap::Parser; +use serde::Deserialize; -#[derive(Parser, Debug, Clone)] +#[derive(Parser, Debug, Clone, Deserialize)] pub struct TelemetryOpts { /// The port on which to expose Prometheus metrics #[clap(short, long, env = "METRICS_PORT", default_value_t = 3300)] - pub metrics_port: u16, + metrics_port: u16, #[clap(short, long, env = "DISABLE_METRICS", default_value_t = false)] - pub disable_metrics: bool, + disable_metrics: bool, +} + +impl TelemetryOpts { + /// Get the metrics port if metrics are enabled or None if they are disabled. + pub fn metrics_port(&self) -> Option { + if self.disable_metrics { + None + } else { + Some(self.metrics_port) + } + } } diff --git a/bolt-sidecar/src/config/validator_indexes.rs b/bolt-sidecar/src/config/validator_indexes.rs index 48e8cca0a..74595ddbf 100644 --- a/bolt-sidecar/src/config/validator_indexes.rs +++ b/bolt-sidecar/src/config/validator_indexes.rs @@ -3,6 +3,8 @@ use std::{ str::FromStr, }; +use serde::{de, Deserialize, Deserializer}; + #[derive(Debug, Clone, Default)] pub struct ValidatorIndexes(Vec); @@ -52,6 +54,16 @@ impl FromStr for ValidatorIndexes { } } +impl<'de> Deserialize<'de> for ValidatorIndexes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + ValidatorIndexes::from_str(&s).map_err(de::Error::custom) + } +} + impl From> for ValidatorIndexes { fn from(vec: Vec) -> Self { Self(vec) diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index ed365244f..4fb806741 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -1,5 +1,8 @@ use core::fmt; -use std::time::{Duration, Instant}; +use std::{ + fs, + time::{Duration, Instant}, +}; use alloy::{rpc::types::beacon::events::HeadEvent, signers::local::PrivateKeySigner}; use beacon_api_client::mainnet::Client as BeaconClient; @@ -19,7 +22,7 @@ use crate::{ crypto::{bls::cl_public_key_to_arr, SignableBLS, SignerECDSA}, primitives::{ CommitmentRequest, ConstraintsMessage, FetchPayloadRequest, LocalPayloadFetcher, - SignedConstraints, TransactionExt, + SignedConstraints, SignedDelegation, TransactionExt, }, signer::{keystore::KeystoreSigner, local::LocalSigner}, start_builder_proxy_server, @@ -132,7 +135,6 @@ impl SidecarDriver { commitment_signer: ECDSA, fetcher: C, ) -> eyre::Result { - let constraints_client = ConstraintsClient::new(opts.constraints_url.clone()); let beacon_client = BeaconClient::new(opts.beacon_api_url.clone()); let execution = ExecutionState::new(fetcher, opts.limits).await?; @@ -169,6 +171,21 @@ impl SidecarDriver { let (api_events_tx, api_events_rx) = mpsc::channel(1024); CommitmentsApiServer::new(api_addr).run(api_events_tx).await; + let mut constraints_client = ConstraintsClient::new(opts.constraints_url.clone()); + + // read the delegaitons from disk if they exist and add them to the constraints client + if let Some(delegations_file_path) = opts.signing.delegations_path.as_ref() { + if let Ok(contents) = fs::read_to_string(delegations_file_path) { + match serde_json::from_str::>(&contents) { + Ok(delegations) => { + info!(count = %delegations.len(), "Loaded signed delegations from disk"); + constraints_client.add_delegations(delegations); + } + Err(err) => error!(?err, "Failed to parse signed delegations from disk"), + } + } + } + Ok(SidecarDriver { head_tracker, execution, diff --git a/bolt-sidecar/src/primitives/mod.rs b/bolt-sidecar/src/primitives/mod.rs index 5a9b04f6b..f963e0f2f 100644 --- a/bolt-sidecar/src/primitives/mod.rs +++ b/bolt-sidecar/src/primitives/mod.rs @@ -24,7 +24,7 @@ use ethereum_consensus::{ Fork, }; use reth_primitives::{BlobTransactionSidecar, Bytes, PooledTransactionsElement, TxKind, TxType}; -use serde::{de, ser::SerializeSeq, Serialize}; +use serde::{de, ser::SerializeSeq}; use tokio::sync::{mpsc, oneshot}; pub use ethereum_consensus::crypto::{PublicKey as BlsPublicKey, Signature as BlsSignature}; @@ -454,7 +454,7 @@ pub struct SignatureError; /// Event types that can be emitted by the validator pubkey to /// signal some action on the Bolt protocol. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] enum SignedMessageAction { /// Signal delegation of a validator pubkey to a delegatee pubkey. @@ -463,13 +463,13 @@ enum SignedMessageAction { Revocation, } -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct SignedDelegation { pub message: DelegationMessage, pub signature: BlsSignature, } -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct DelegationMessage { action: u8, pub validator_pubkey: BlsPublicKey, @@ -494,13 +494,13 @@ impl SignableBLS for DelegationMessage { } } -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] pub struct SignedRevocation { pub message: RevocationMessage, pub signature: BlsSignature, } -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] pub struct RevocationMessage { action: u8, pub validator_pubkey: BlsPublicKey,