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/Config.example.toml b/bolt-sidecar/Config.example.toml new file mode 100644 index 000000000..05f97692a --- /dev/null +++ b/bolt-sidecar/Config.example.toml @@ -0,0 +1,20 @@ +# ports +port = 8000 +metrics_port = 3300 + +# node urls +execution_api_url = "http://localhost:8545" +beacon_api_url = "http://localhost:5052" +engine_api_url = "http://localhost:8551" + +# constraints options +constraints_url = "http://localhost:3030" +constraints_proxy_port = 18551 + +# chain options +chain = "kurtosis" +slot_time = 2 + +# signing options +private_key = "0x359c156600e1f5715a58c9e09f17c8d04e7ee3a9f88b39f6e489ffca0148fabe" +delegations_path = "./delegations.json" 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/builder.rs b/bolt-sidecar/src/api/builder.rs index d32e679d8..8a4b91293 100644 --- a/bolt-sidecar/src/api/builder.rs +++ b/bolt-sidecar/src/api/builder.rs @@ -1,3 +1,5 @@ +use std::{sync::Arc, time::Duration}; + use axum::{ body::{self, Body}, extract::{Path, Request, State}, @@ -16,7 +18,6 @@ use ethereum_consensus::{ use parking_lot::Mutex; use reqwest::Url; use serde::Deserialize; -use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::net::TcpListener; use tracing::{debug, error, info, warn}; @@ -26,8 +27,9 @@ use super::spec::{ STATUS_PATH, }; use crate::{ + builder::payload_fetcher::PayloadFetcher, client::constraints_client::ConstraintsClient, - primitives::{GetPayloadResponse, PayloadFetcher, SignedBuilderBid}, + primitives::{GetPayloadResponse, SignedBuilderBid}, telemetry::ApiMetrics, }; diff --git a/bolt-sidecar/src/api/commitments/handlers.rs b/bolt-sidecar/src/api/commitments/handlers.rs index 36626a514..fcdc557ef 100644 --- a/bolt-sidecar/src/api/commitments/handlers.rs +++ b/bolt-sidecar/src/api/commitments/handlers.rs @@ -12,8 +12,9 @@ use serde_json::Value; use tracing::{debug, error, info, instrument}; use crate::{ - commitments::headers::auth_from_headers, common::CARGO_PKG_VERSION, - primitives::InclusionRequest, + commitments::headers::auth_from_headers, + common::CARGO_PKG_VERSION, + primitives::{commitment::SignatureError, InclusionRequest}, }; use super::{ @@ -70,7 +71,7 @@ pub async fn rpc_entrypoint( "Recovered signer does not match the provided signer" ); - return Err(Error::InvalidSignature(crate::primitives::SignatureError)); + return Err(Error::InvalidSignature(SignatureError)); } // Set the request signer diff --git a/bolt-sidecar/src/api/commitments/headers.rs b/bolt-sidecar/src/api/commitments/headers.rs index e609616a4..79ad3de34 100644 --- a/bolt-sidecar/src/api/commitments/headers.rs +++ b/bolt-sidecar/src/api/commitments/headers.rs @@ -3,6 +3,8 @@ use std::str::FromStr; use alloy::primitives::{Address, Signature}; use axum::http::HeaderMap; +use crate::primitives::commitment::SignatureError; + use super::spec::{Error, SIGNATURE_HEADER}; /// Extracts the signature ([SIGNATURE_HEADER]) from the HTTP headers. @@ -19,8 +21,7 @@ pub fn auth_from_headers(headers: &HeaderMap) -> Result<(Address, Signature), Er let address = Address::from_str(address).map_err(|_| Error::MalformedHeader)?; let sig = split.next().ok_or(Error::MalformedHeader)?; - let sig = Signature::from_str(sig) - .map_err(|_| Error::InvalidSignature(crate::primitives::SignatureError))?; + let sig = Signature::from_str(sig).map_err(|_| Error::InvalidSignature(SignatureError))?; Ok((address, sig)) } diff --git a/bolt-sidecar/src/api/commitments/spec.rs b/bolt-sidecar/src/api/commitments/spec.rs index 5a4f67c79..d499e69a4 100644 --- a/bolt-sidecar/src/api/commitments/spec.rs +++ b/bolt-sidecar/src/api/commitments/spec.rs @@ -40,7 +40,7 @@ pub enum Error { NoSignature, /// Invalid signature. #[error(transparent)] - InvalidSignature(#[from] crate::primitives::SignatureError), + InvalidSignature(#[from] crate::primitives::commitment::SignatureError), /// Malformed authentication header. #[error("Malformed authentication header")] MalformedHeader, 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/builder/mod.rs b/bolt-sidecar/src/builder/mod.rs index 1ea89bb76..d63277a46 100644 --- a/bolt-sidecar/src/builder/mod.rs +++ b/bolt-sidecar/src/builder/mod.rs @@ -5,8 +5,6 @@ use ethereum_consensus::{ deneb::mainnet::ExecutionPayloadHeader, ssz::prelude::{List, MerkleizationError}, }; -use payload_builder::FallbackPayloadBuilder; -use signature::sign_builder_message; use crate::{ common::BlsSecretKeyWrapper, @@ -23,16 +21,21 @@ pub use template::BlockTemplate; /// Builder payload signing utilities pub mod signature; +use signature::sign_builder_message; + +/// Fallback Payload builder agent that leverages the engine API's +/// `engine_newPayloadV3` response error to produce a valid payload. +pub mod payload_builder; +use payload_builder::FallbackPayloadBuilder; + +/// Interface for fetching payloads from the beacon node. +pub mod payload_fetcher; /// Compatibility types and utilities between Alloy, Reth, /// Ethereum-consensus and other crates. #[doc(hidden)] mod compat; -/// Fallback Payload builder agent that leverages the engine API's -/// `engine_newPayloadV3` response error to produce a valid payload. -pub mod payload_builder; - #[derive(Debug, thiserror::Error)] #[non_exhaustive] #[allow(missing_docs)] diff --git a/bolt-sidecar/src/builder/payload_fetcher.rs b/bolt-sidecar/src/builder/payload_fetcher.rs new file mode 100644 index 000000000..b5df157fc --- /dev/null +++ b/bolt-sidecar/src/builder/payload_fetcher.rs @@ -0,0 +1,57 @@ +use tokio::sync::{mpsc, oneshot}; +use tracing::error; + +use crate::primitives::{FetchPayloadRequest, PayloadAndBid}; + +/// A local payload fetcher that sends requests to a channel +/// and waits for a response on a oneshot channel. +#[derive(Debug, Clone)] +pub struct LocalPayloadFetcher { + tx: mpsc::Sender, +} + +impl LocalPayloadFetcher { + /// Create a new `LocalPayloadFetcher` with the given channel to send fetch requests. + pub fn new(tx: mpsc::Sender) -> Self { + Self { tx } + } +} + +#[async_trait::async_trait] +impl PayloadFetcher for LocalPayloadFetcher { + async fn fetch_payload(&self, slot: u64) -> Option { + let (response_tx, response_rx) = oneshot::channel(); + + let fetch_params = FetchPayloadRequest { response_tx, slot }; + self.tx.send(fetch_params).await.ok()?; + + match response_rx.await { + Ok(res) => res, + Err(e) => { + error!(err = ?e, "Failed to fetch payload"); + None + } + } + } +} + +/// Interface for fetching payloads for the builder. +#[async_trait::async_trait] +pub trait PayloadFetcher { + /// Fetch a payload for the given slot. + async fn fetch_payload(&self, slot: u64) -> Option; +} + +/// A payload fetcher that does nothing, used for testing. +#[derive(Debug)] +#[cfg(test)] +pub struct NoopPayloadFetcher; + +#[cfg(test)] +#[async_trait::async_trait] +impl PayloadFetcher for NoopPayloadFetcher { + async fn fetch_payload(&self, slot: u64) -> Option { + tracing::info!(slot, "Fetch payload called"); + None + } +} diff --git a/bolt-sidecar/src/client/constraints_client.rs b/bolt-sidecar/src/client/constraints_client.rs index 67b49866d..90730361c 100644 --- a/bolt-sidecar/src/client/constraints_client.rs +++ b/bolt-sidecar/src/client/constraints_client.rs @@ -2,10 +2,13 @@ //! The Bolt sidecar's main purpose is to sit between the beacon node and Constraints client, //! so most requests are simply proxied to its API. +use std::collections::HashSet; + use axum::http::StatusCode; use beacon_api_client::VersionedValue; use ethereum_consensus::{ - builder::SignedValidatorRegistration, deneb::mainnet::SignedBlindedBeaconBlock, Fork, + builder::SignedValidatorRegistration, crypto::PublicKey as BlsPublicKey, + deneb::mainnet::SignedBlindedBeaconBlock, Fork, }; use reqwest::Url; use tracing::error; @@ -30,6 +33,7 @@ use crate::{ pub struct ConstraintsClient { url: Url, client: reqwest::Client, + delegations: Vec, } impl ConstraintsClient { @@ -38,9 +42,24 @@ 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); + } + + /// Finds all delegations for the given public key. + pub fn find_delegatees(&self, pubkey: &BlsPublicKey) -> HashSet { + self.delegations + .iter() + .filter(|d| d.message.delegatee_pubkey == *pubkey) + .map(|d| d.message.delegatee_pubkey.clone()) + .collect::>() + } + 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 +99,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 +216,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 +233,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..b33561072 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..93dfa0693 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,28 +30,26 @@ 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 - #[clap(long, env = "BOLT_SIDECAR_PORT", - default_value_t = DEFAULT_RPC_PORT)] + #[clap(long, env = "BOLT_SIDECAR_PORT", default_value_t = DEFAULT_RPC_PORT)] pub port: u16, - /// URL for the beacon client - #[clap(long, env = "BOLT_SIDECAR_BEACON_API_URL", default_value = "http://localhost:5052")] - pub beacon_api_url: Url, - /// URL for the Constraint sidecar client to use - #[clap(long, env = "BOLT_SIDECAR_CONSTRAINTS_URL", default_value = "http://localhost:3030")] - pub constraints_url: Url, /// Execution client API URL #[clap(long, env = "BOLT_SIDECAR_EXECUTION_API_URL", default_value = "http://localhost:8545")] pub execution_api_url: Url, + /// URL for the beacon client + #[clap(long, env = "BOLT_SIDECAR_BEACON_API_URL", default_value = "http://localhost:5052")] + pub beacon_api_url: Url, /// Execution client Engine API URL #[clap(long, env = "BOLT_SIDECAR_ENGINE_API_URL", default_value = "http://localhost:8551")] pub engine_api_url: Url, + /// URL for the Constraint sidecar client to use + #[clap(long, env = "BOLT_SIDECAR_CONSTRAINTS_URL", default_value = "http://localhost:3030")] + pub constraints_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: @@ -63,13 +65,10 @@ pub struct Opts { #[clap(long, env = "BOLT_SIDECAR_JWT_HEX", default_value_t)] pub jwt_hex: JwtSecretConfig, /// The fee recipient address for fallback blocks - #[clap(long, env = "BOLT_SIDECAR_FEE_RECIPIENT", - default_value_t = Address::ZERO)] + #[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,16 +89,42 @@ 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::*; #[test] - fn parse_url() { + fn test_parse_url() { let url = "http://0.0.0.0:3030"; let parsed = url.parse::().unwrap(); let socket_addr = parsed.socket_addrs(|| None).unwrap()[0]; let localhost_socket = "0.0.0.0:3030".parse().unwrap(); assert_eq!(socket_addr, localhost_socket); } + + #[test] + fn test_parse_config_from_toml() { + let path = env!("CARGO_MANIFEST_DIR").to_string() + "Config.toml"; + + if let Ok(config_file) = std::fs::read_to_string(path) { + let config = Opts::parse_from_toml(&config_file).expect("Failed to parse config"); + assert_eq!(config.execution_api_url, Url::parse("http://localhost:8545").unwrap()); + assert_eq!(config.beacon_api_url, Url::parse("http://localhost:5052").unwrap()); + assert_eq!(config.engine_api_url, Url::parse("http://localhost:8551").unwrap()); + assert_eq!(config.constraints_url, Url::parse("http://localhost:3030").unwrap()); + assert_eq!(config.constraints_proxy_port, 18551); + } + } } diff --git a/bolt-sidecar/src/config/signing.rs b/bolt-sidecar/src/config/signing.rs index d4d91494d..beb8f2fe3 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 @@ -40,18 +42,8 @@ impl fmt::Debug for SigningOpts { .field("commit_boost_url", &self.commit_boost_address) .field("commit_boost_jwt_hex", &self.commit_boost_jwt_hex) .field("keystore_password", &"********") // Hides the actual password + .field("keystore_path", &self.keystore_path) + .field("delegations_path", &self.delegations_path) .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..77dcb6718 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -1,10 +1,14 @@ -use core::fmt; -use std::time::{Duration, Instant}; +use std::{ + collections::HashSet, + fmt, + time::{Duration, Instant}, +}; use alloy::{rpc::types::beacon::events::HeadEvent, signers::local::PrivateKeySigner}; use beacon_api_client::mainnet::Client as BeaconClient; use ethereum_consensus::{ clock::{self, SlotStream, SystemTimeProvider}, + crypto::bls::PublicKey as BlsPublicKey, phase0::mainnet::SLOTS_PER_EPOCH, }; use futures::StreamExt; @@ -12,14 +16,15 @@ use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use crate::{ + builder::payload_fetcher::LocalPayloadFetcher, commitments::{ server::{CommitmentsApiServer, Event as CommitmentEvent}, spec::Error as CommitmentError, }, crypto::{bls::cl_public_key_to_arr, SignableBLS, SignerECDSA}, primitives::{ - CommitmentRequest, ConstraintsMessage, FetchPayloadRequest, LocalPayloadFetcher, - SignedConstraints, TransactionExt, + read_signed_delegations_from_file, CommitmentRequest, ConstraintsMessage, + FetchPayloadRequest, SignedConstraints, TransactionExt, }, signer::{keystore::KeystoreSigner, local::LocalSigner}, start_builder_proxy_server, @@ -30,15 +35,32 @@ use crate::{ }; /// The driver for the sidecar, responsible for managing the main event loop. +/// +/// The reponsibilities of the driver include: +/// - Handling incoming API events +/// - Updating the execution state based on new beacon chain heads +/// - Submitting constraints to the constraints service at the commitment deadline +/// - Building local payloads for the beacon chain +/// - Responding to requests to fetch a local payload +/// - Updating the consensus state based on the beacon chain clock pub struct SidecarDriver { + /// Head tracker for monitoring the beacon chain clock head_tracker: HeadTracker, + /// Execution state for tracking the current head and block templates execution: ExecutionState, + /// Consensus state for tracking the current slot and validator indexes consensus: ConsensusState, + /// Signer for creating constraints constraint_signer: SignerBLS, + /// Signer for creating commitment responses commitment_signer: ECDSA, + /// Local block builder for creating local payloads local_builder: LocalBuilder, + /// Client for interacting with the constraints service constraints_client: ConstraintsClient, + /// Channel for receiving incoming API events api_events_rx: mpsc::Receiver, + /// Channel for receiving requests to fetch a local payload payload_requests_rx: mpsc::Receiver, /// Stream of slots made from the consensus clock slot_stream: SlotStream, @@ -107,20 +129,13 @@ impl SidecarDriver { let state_client = StateClient::new(opts.execution_api_url.clone()); let commit_boost_signer = CommitBoostSigner::new( - opts.signing - .commit_boost_address - .expect("CommitBoost URL must be provided") - .to_string(), - &opts.signing.commit_boost_jwt_hex.clone().expect("CommitBoost JWT must be provided"), - ) - .await?; + opts.signing.commit_boost_address.expect("CommitBoost URL").to_string(), + &opts.signing.commit_boost_jwt_hex.clone().expect("CommitBoost JWT"), + )?; let cb_bls_signer = SignerBLS::CommitBoost(commit_boost_signer.clone()); - // Commitment responses are signed with commit-boost signer - let commitment_signer = commit_boost_signer.clone(); - - Self::from_components(opts, cb_bls_signer, commitment_signer, state_client).await + Self::from_components(opts, cb_bls_signer, commit_boost_signer, state_client).await } } @@ -132,7 +147,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 +183,14 @@ 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() { + let delegations = read_signed_delegations_from_file(delegations_file_path)?; + constraints_client.add_delegations(delegations); + } + Ok(SidecarDriver { head_tracker, execution, @@ -220,7 +242,7 @@ impl SidecarDriver { let start = Instant::now(); let validator_pubkey = match self.consensus.validate_request(&request) { - Ok(index) => index, + Ok(pubkey) => pubkey, Err(err) => { error!(?err, "Consensus: failed to validate request"); let _ = response.send(Err(CommitmentError::Consensus(err))); @@ -245,13 +267,13 @@ impl SidecarDriver { "Validation against execution state passed" ); - // parse the request into constraints and sign them - let slot = inclusion_request.slot; + let delegatees = self.constraints_client.find_delegatees(&validator_pubkey); + let available_pubkeys = self.constraint_signer.available_pubkeys(); - let pubkey = match self.constraint_signer { - SignerBLS::Local(ref signer) => signer.pubkey(), - SignerBLS::CommitBoost(ref signer) => signer.pubkey(), - SignerBLS::Keystore(_) => validator_pubkey.clone(), + let Some(pubkey) = pick_public_key(validator_pubkey, available_pubkeys, delegatees) else { + error!(%target_slot, "No available public key to sign constraints with"); + let _ = response.send(Err(CommitmentError::Internal)); + return; }; // NOTE: we iterate over the transactions in the request and generate a signed constraint @@ -260,7 +282,7 @@ impl SidecarDriver { // with no ordering guarantees. for tx in inclusion_request.txs { let tx_type = tx.tx_type(); - let message = ConstraintsMessage::from_transaction(pubkey.clone(), slot, tx); + let message = ConstraintsMessage::from_transaction(pubkey.clone(), target_slot, tx); let digest = message.digest(); let signature = match self.constraint_signer { @@ -281,12 +303,15 @@ impl SidecarDriver { }; ApiMetrics::increment_transactions_preconfirmed(tx_type); - self.execution.add_constraint(slot, signed_constraints); + self.execution.add_constraint(target_slot, signed_constraints); } // Create a commitment by signing the request match request.commit_and_sign(&self.commitment_signer).await { - Ok(commitment) => response.send(Ok(commitment)).ok(), + Ok(commitment) => { + debug!(target_slot, elapsed = ?start.elapsed(), "Commitment signed and sent"); + response.send(Ok(commitment)).ok() + } Err(err) => { error!(?err, "Failed to sign commitment"); response.send(Err(CommitmentError::Internal)).ok() @@ -354,3 +379,29 @@ impl SidecarDriver { } } } + +/// Pick a pubkey to sign constraints with. +/// +/// Rationale: +/// - If there are no delegatee keys, try to use the validator key directly if available. +/// - If there are delegatee keys, try to use the first one that is available in the list. +fn pick_public_key( + validator: BlsPublicKey, + available: HashSet, + delegatees: HashSet, +) -> Option { + if delegatees.is_empty() { + if available.contains(&validator) { + return Some(validator); + } else { + return None; + } + } else { + for delegatee in delegatees { + if available.contains(&delegatee) { + return Some(delegatee); + } + } + } + None +} diff --git a/bolt-sidecar/src/primitives/commitment.rs b/bolt-sidecar/src/primitives/commitment.rs index 28d06046d..403808550 100644 --- a/bolt-sidecar/src/primitives/commitment.rs +++ b/bolt-sidecar/src/primitives/commitment.rs @@ -3,12 +3,13 @@ use std::str::FromStr; use alloy::primitives::{keccak256, Address, Signature, B256}; -use crate::{ - crypto::SignerECDSA, - primitives::{deserialize_txs, serialize_txs}, -}; +use crate::crypto::SignerECDSA; -use super::{FullTransaction, SignatureError, TransactionExt}; +use super::{deserialize_txs, serialize_txs, FullTransaction, TransactionExt}; + +#[derive(Debug, thiserror::Error)] +#[error("Invalid signature")] +pub struct SignatureError; /// Commitment requests sent by users or RPC proxies to the sidecar. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/bolt-sidecar/src/primitives/constraint.rs b/bolt-sidecar/src/primitives/constraint.rs index 344bf7807..1df85a547 100644 --- a/bolt-sidecar/src/primitives/constraint.rs +++ b/bolt-sidecar/src/primitives/constraint.rs @@ -2,12 +2,9 @@ use alloy::signers::k256::sha2::{Digest, Sha256}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{bls::BLSSig, SignableBLS}, - primitives::{deserialize_txs, serialize_txs}, -}; +use crate::crypto::{bls::BLSSig, SignableBLS}; -use super::{FullTransaction, InclusionRequest}; +use super::{deserialize_txs, serialize_txs, FullTransaction, InclusionRequest}; /// The inclusion request transformed into an explicit list of signed constraints /// that need to be forwarded to the PBS pipeline to inform block production. diff --git a/bolt-sidecar/src/primitives/delegation.rs b/bolt-sidecar/src/primitives/delegation.rs new file mode 100644 index 000000000..99a3b7267 --- /dev/null +++ b/bolt-sidecar/src/primitives/delegation.rs @@ -0,0 +1,107 @@ +use std::fs; + +use alloy::signers::k256::sha2::{Digest, Sha256}; +use ethereum_consensus::crypto::{PublicKey as BlsPublicKey, Signature as BlsSignature}; + +use crate::crypto::SignableBLS; + +/// Event types that can be emitted by the validator pubkey to +/// signal some action on the Bolt protocol. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SignedMessageAction { + /// Signal delegation of a validator pubkey to a delegatee pubkey. + Delegation, + /// Signal revocation of a previously delegated pubkey. + Revocation, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct SignedDelegation { + pub message: DelegationMessage, + pub signature: BlsSignature, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct DelegationMessage { + action: u8, + pub validator_pubkey: BlsPublicKey, + pub delegatee_pubkey: BlsPublicKey, +} + +impl DelegationMessage { + /// Create a new delegation message. + pub fn new(validator_pubkey: BlsPublicKey, delegatee_pubkey: BlsPublicKey) -> Self { + Self { action: SignedMessageAction::Delegation as u8, validator_pubkey, delegatee_pubkey } + } +} + +impl SignableBLS for DelegationMessage { + fn digest(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update([self.action]); + hasher.update(self.validator_pubkey.to_vec()); + hasher.update(self.delegatee_pubkey.to_vec()); + + hasher.finalize().into() + } +} + +/// read the delegaitons from disk if they exist and add them to the constraints client +pub fn read_signed_delegations_from_file(file_path: &str) -> eyre::Result> { + match fs::read_to_string(file_path) { + Ok(contents) => match serde_json::from_str::>(&contents) { + Ok(delegations) => Ok(delegations), + Err(err) => Err(eyre::eyre!("Failed to parse signed delegations from disk: {:?}", err)), + }, + Err(err) => Err(eyre::eyre!("Failed to read signed delegations from disk: {:?}", err)), + } +} + +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] +pub struct SignedRevocation { + pub message: RevocationMessage, + pub signature: BlsSignature, +} + +#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)] +pub struct RevocationMessage { + action: u8, + pub validator_pubkey: BlsPublicKey, + pub delegatee_pubkey: BlsPublicKey, +} + +impl RevocationMessage { + /// Create a new revocation message. + pub fn new(validator_pubkey: BlsPublicKey, delegatee_pubkey: BlsPublicKey) -> Self { + Self { action: SignedMessageAction::Revocation as u8, validator_pubkey, delegatee_pubkey } + } +} + +impl SignableBLS for RevocationMessage { + fn digest(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update([self.action]); + hasher.update(self.validator_pubkey.to_vec()); + hasher.update(self.delegatee_pubkey.to_vec()); + + hasher.finalize().into() + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_read_signed_delegations_from_file() { + let file = env!("CARGO_MANIFEST_DIR").to_string() + "/test_data/delegations.json"; + + let delegations = super::read_signed_delegations_from_file(&file) + .expect("Failed to read delegations from file"); + + assert_eq!(delegations.len(), 1); + assert_eq!( + format!("{:?}", delegations[0].message.validator_pubkey), + "0x83b85769a8f2a1a6bd3a609e51b460f6fb897daff1157991479421493926faeffa6670152524403929a8a7e551d345f3" + ); + } +} diff --git a/bolt-sidecar/src/primitives/mod.rs b/bolt-sidecar/src/primitives/mod.rs index 5a9b04f6b..3c70f6990 100644 --- a/bolt-sidecar/src/primitives/mod.rs +++ b/bolt-sidecar/src/primitives/mod.rs @@ -1,15 +1,9 @@ // TODO: add docs #![allow(missing_docs)] -use std::{ - borrow::Cow, - sync::{atomic::AtomicU64, Arc}, -}; +use std::sync::{atomic::AtomicU64, Arc}; -use alloy::{ - primitives::{Address, U256}, - signers::k256::sha2::{Digest, Sha256}, -}; +use alloy::primitives::U256; use ethereum_consensus::{ crypto::KzgCommitment, deneb::{ @@ -23,9 +17,8 @@ use ethereum_consensus::{ types::mainnet::ExecutionPayload, Fork, }; -use reth_primitives::{BlobTransactionSidecar, Bytes, PooledTransactionsElement, TxKind, TxType}; -use serde::{de, ser::SerializeSeq, Serialize}; -use tokio::sync::{mpsc, oneshot}; + +use tokio::sync::oneshot; pub use ethereum_consensus::crypto::{PublicKey as BlsPublicKey, Signature as BlsSignature}; @@ -37,9 +30,17 @@ pub use commitment::{CommitmentRequest, InclusionRequest}; /// for validation. pub mod constraint; pub use constraint::{BatchedSignedConstraints, ConstraintsMessage, SignedConstraints}; -use tracing::{error, info}; -use crate::crypto::SignableBLS; +/// Delegation and revocation signed message types and utilities. +pub mod delegation; +pub use delegation::{ + read_signed_delegations_from_file, DelegationMessage, RevocationMessage, SignedDelegation, + SignedRevocation, +}; + +/// Transaction types and extension utilities. +pub mod transaction; +pub use transaction::{deserialize_txs, serialize_txs, FullTransaction, TransactionExt}; /// An alias for a Beacon Chain slot number pub type Slot = u64; @@ -112,51 +113,6 @@ pub struct PayloadAndBid { pub payload: GetPayloadResponse, } -#[derive(Debug, Clone)] -pub struct LocalPayloadFetcher { - tx: mpsc::Sender, -} - -impl LocalPayloadFetcher { - pub fn new(tx: mpsc::Sender) -> Self { - Self { tx } - } -} - -#[async_trait::async_trait] -impl PayloadFetcher for LocalPayloadFetcher { - async fn fetch_payload(&self, slot: u64) -> Option { - let (response_tx, response_rx) = oneshot::channel(); - - let fetch_params = FetchPayloadRequest { response_tx, slot }; - self.tx.send(fetch_params).await.ok()?; - - match response_rx.await { - Ok(res) => res, - Err(e) => { - error!(err = ?e, "Failed to fetch payload"); - None - } - } - } -} - -#[async_trait::async_trait] -pub trait PayloadFetcher { - async fn fetch_payload(&self, slot: u64) -> Option; -} - -#[derive(Debug)] -pub struct NoopPayloadFetcher; - -#[async_trait::async_trait] -impl PayloadFetcher for NoopPayloadFetcher { - async fn fetch_payload(&self, slot: u64) -> Option { - info!(slot, "Fetch payload called"); - None - } -} - /// TODO: implement SSZ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PayloadAndBlobs { @@ -244,283 +200,3 @@ impl ChainHead { self.block.load(std::sync::atomic::Ordering::SeqCst) } } - -/// Trait that exposes additional information on transaction types that don't already do it -/// by themselves (e.g. [`PooledTransactionsElement`]). -pub trait TransactionExt { - fn gas_limit(&self) -> u64; - fn value(&self) -> U256; - fn tx_type(&self) -> TxType; - fn tx_kind(&self) -> TxKind; - fn input(&self) -> &Bytes; - fn chain_id(&self) -> Option; - fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar>; - fn size(&self) -> usize; -} - -impl TransactionExt for PooledTransactionsElement { - fn gas_limit(&self) -> u64 { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => transaction.gas_limit, - PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.gas_limit, - PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.gas_limit, - PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.gas_limit, - _ => unimplemented!(), - } - } - - fn value(&self) -> U256 { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => transaction.value, - PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.value, - PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.value, - PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.value, - _ => unimplemented!(), - } - } - - fn tx_type(&self) -> TxType { - match self { - PooledTransactionsElement::Legacy { .. } => TxType::Legacy, - PooledTransactionsElement::Eip2930 { .. } => TxType::Eip2930, - PooledTransactionsElement::Eip1559 { .. } => TxType::Eip1559, - PooledTransactionsElement::BlobTransaction(_) => TxType::Eip4844, - _ => unimplemented!(), - } - } - - fn tx_kind(&self) -> TxKind { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => transaction.to, - PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.to, - PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.to, - PooledTransactionsElement::BlobTransaction(blob_tx) => { - TxKind::Call(blob_tx.transaction.to) - } - _ => unimplemented!(), - } - } - - fn input(&self) -> &Bytes { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => &transaction.input, - PooledTransactionsElement::Eip2930 { transaction, .. } => &transaction.input, - PooledTransactionsElement::Eip1559 { transaction, .. } => &transaction.input, - PooledTransactionsElement::BlobTransaction(blob_tx) => &blob_tx.transaction.input, - _ => unimplemented!(), - } - } - - fn chain_id(&self) -> Option { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => transaction.chain_id, - PooledTransactionsElement::Eip2930 { transaction, .. } => Some(transaction.chain_id), - PooledTransactionsElement::Eip1559 { transaction, .. } => Some(transaction.chain_id), - PooledTransactionsElement::BlobTransaction(blob_tx) => { - Some(blob_tx.transaction.chain_id) - } - _ => unimplemented!(), - } - } - - fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { - match self { - PooledTransactionsElement::BlobTransaction(blob_tx) => Some(&blob_tx.sidecar), - _ => None, - } - } - - fn size(&self) -> usize { - match self { - PooledTransactionsElement::Legacy { transaction, .. } => transaction.size(), - PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.size(), - PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.size(), - PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.size(), - _ => unimplemented!(), - } - } -} - -pub const fn tx_type_str(tx_type: TxType) -> &'static str { - match tx_type { - TxType::Legacy => "legacy", - TxType::Eip2930 => "eip2930", - TxType::Eip1559 => "eip1559", - TxType::Eip4844 => "eip4844", - TxType::Eip7702 => "eip7702", - } -} - -/// A wrapper type for a full, complete transaction (i.e. with blob sidecars attached). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FullTransaction { - pub tx: PooledTransactionsElement, - sender: Option
, -} - -impl From for FullTransaction { - fn from(tx: PooledTransactionsElement) -> Self { - Self { tx, sender: None } - } -} - -impl std::ops::Deref for FullTransaction { - type Target = PooledTransactionsElement; - - fn deref(&self) -> &Self::Target { - &self.tx - } -} - -impl std::ops::DerefMut for FullTransaction { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.tx - } -} - -impl FullTransaction { - /// Convenience method to parse a raw transaction into a `FullTransaction`. - pub fn decode_enveloped(data: impl AsRef<[u8]>) -> eyre::Result { - let tx = PooledTransactionsElement::decode_enveloped(&mut data.as_ref())?; - Ok(Self { tx, sender: None }) - } - - pub fn into_inner(self) -> PooledTransactionsElement { - self.tx - } - - /// Returns the sender of the transaction, if recovered. - pub fn sender(&self) -> Option<&Address> { - self.sender.as_ref() - } - - /// Returns the effective miner gas tip cap (`gasTipCap`) for the given base fee: - /// `min(maxFeePerGas - baseFee, maxPriorityFeePerGas)` - /// - /// Returns `None` if the basefee is higher than the [`Transaction::max_fee_per_gas`]. - /// Ref: https://github.com/paradigmxyz/reth/blob/2d592125128c3742ff97b321884f93f9063abcb2/crates/primitives/src/transaction/mod.rs#L444 - pub fn effective_tip_per_gas(&self, base_fee: u128) -> Option { - let max_fee_per_gas = self.max_fee_per_gas(); - - if max_fee_per_gas < base_fee { - return None; - } - - // Calculate the difference between max_fee_per_gas and base_fee - let fee = max_fee_per_gas - base_fee; - - // Compare the fee with max_priority_fee_per_gas (or gas price for non-EIP1559 transactions) - if let Some(priority_fee) = self.max_priority_fee_per_gas() { - Some(fee.min(priority_fee)) - } else { - Some(fee) - } - } -} - -fn serialize_txs( - txs: &[FullTransaction], - serializer: S, -) -> Result { - let mut seq = serializer.serialize_seq(Some(txs.len()))?; - for tx in txs { - let encoded = tx.tx.envelope_encoded(); - seq.serialize_element(&format!("0x{}", hex::encode(encoded)))?; - } - seq.end() -} - -fn deserialize_txs<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let hex_strings = > as de::Deserialize>::deserialize(deserializer)?; - let mut txs = Vec::with_capacity(hex_strings.len()); - - for s in hex_strings { - let data = hex::decode(s.trim_start_matches("0x")).map_err(de::Error::custom)?; - let tx = PooledTransactionsElement::decode_enveloped(&mut data.as_slice()) - .map_err(de::Error::custom) - .map(|tx| FullTransaction { tx, sender: None })?; - txs.push(tx); - } - - Ok(txs) -} - -#[derive(Debug, thiserror::Error)] -#[error("Invalid signature")] -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)] -#[repr(u8)] -enum SignedMessageAction { - /// Signal delegation of a validator pubkey to a delegatee pubkey. - Delegation, - /// Signal revocation of a previously delegated pubkey. - Revocation, -} - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -pub struct SignedDelegation { - pub message: DelegationMessage, - pub signature: BlsSignature, -} - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -pub struct DelegationMessage { - action: u8, - pub validator_pubkey: BlsPublicKey, - pub delegatee_pubkey: BlsPublicKey, -} - -impl DelegationMessage { - /// Create a new delegation message. - pub fn new(validator_pubkey: BlsPublicKey, delegatee_pubkey: BlsPublicKey) -> Self { - Self { action: SignedMessageAction::Delegation as u8, validator_pubkey, delegatee_pubkey } - } -} - -impl SignableBLS for DelegationMessage { - fn digest(&self) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update([self.action]); - hasher.update(self.validator_pubkey.to_vec()); - hasher.update(self.delegatee_pubkey.to_vec()); - - hasher.finalize().into() - } -} - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -pub struct SignedRevocation { - pub message: RevocationMessage, - pub signature: BlsSignature, -} - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -pub struct RevocationMessage { - action: u8, - pub validator_pubkey: BlsPublicKey, - pub delegatee_pubkey: BlsPublicKey, -} - -impl RevocationMessage { - /// Create a new revocation message. - pub fn new(validator_pubkey: BlsPublicKey, delegatee_pubkey: BlsPublicKey) -> Self { - Self { action: SignedMessageAction::Revocation as u8, validator_pubkey, delegatee_pubkey } - } -} - -impl SignableBLS for RevocationMessage { - fn digest(&self) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update([self.action]); - hasher.update(self.validator_pubkey.to_vec()); - hasher.update(self.delegatee_pubkey.to_vec()); - - hasher.finalize().into() - } -} diff --git a/bolt-sidecar/src/primitives/transaction.rs b/bolt-sidecar/src/primitives/transaction.rs new file mode 100644 index 000000000..3f18397fa --- /dev/null +++ b/bolt-sidecar/src/primitives/transaction.rs @@ -0,0 +1,210 @@ +use std::borrow::Cow; + +use alloy::primitives::{Address, U256}; +use reth_primitives::{BlobTransactionSidecar, Bytes, PooledTransactionsElement, TxKind, TxType}; +use serde::{de, ser::SerializeSeq}; + +/// Trait that exposes additional information on transaction types that don't already do it +/// by themselves (e.g. [`PooledTransactionsElement`]). +pub trait TransactionExt { + fn gas_limit(&self) -> u64; + fn value(&self) -> U256; + fn tx_type(&self) -> TxType; + fn tx_kind(&self) -> TxKind; + fn input(&self) -> &Bytes; + fn chain_id(&self) -> Option; + fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar>; + fn size(&self) -> usize; +} + +impl TransactionExt for PooledTransactionsElement { + fn gas_limit(&self) -> u64 { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.gas_limit, + _ => unimplemented!(), + } + } + + fn value(&self) -> U256 { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.value, + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.value, + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.value, + PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.value, + _ => unimplemented!(), + } + } + + fn tx_type(&self) -> TxType { + match self { + PooledTransactionsElement::Legacy { .. } => TxType::Legacy, + PooledTransactionsElement::Eip2930 { .. } => TxType::Eip2930, + PooledTransactionsElement::Eip1559 { .. } => TxType::Eip1559, + PooledTransactionsElement::BlobTransaction(_) => TxType::Eip4844, + _ => unimplemented!(), + } + } + + fn tx_kind(&self) -> TxKind { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.to, + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.to, + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.to, + PooledTransactionsElement::BlobTransaction(blob_tx) => { + TxKind::Call(blob_tx.transaction.to) + } + _ => unimplemented!(), + } + } + + fn input(&self) -> &Bytes { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => &transaction.input, + PooledTransactionsElement::Eip2930 { transaction, .. } => &transaction.input, + PooledTransactionsElement::Eip1559 { transaction, .. } => &transaction.input, + PooledTransactionsElement::BlobTransaction(blob_tx) => &blob_tx.transaction.input, + _ => unimplemented!(), + } + } + + fn chain_id(&self) -> Option { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.chain_id, + PooledTransactionsElement::Eip2930 { transaction, .. } => Some(transaction.chain_id), + PooledTransactionsElement::Eip1559 { transaction, .. } => Some(transaction.chain_id), + PooledTransactionsElement::BlobTransaction(blob_tx) => { + Some(blob_tx.transaction.chain_id) + } + _ => unimplemented!(), + } + } + + fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { + match self { + PooledTransactionsElement::BlobTransaction(blob_tx) => Some(&blob_tx.sidecar), + _ => None, + } + } + + fn size(&self) -> usize { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.size(), + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.size(), + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.size(), + PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.size(), + _ => unimplemented!(), + } + } +} + +pub const fn tx_type_str(tx_type: TxType) -> &'static str { + match tx_type { + TxType::Legacy => "legacy", + TxType::Eip2930 => "eip2930", + TxType::Eip1559 => "eip1559", + TxType::Eip4844 => "eip4844", + TxType::Eip7702 => "eip7702", + } +} + +/// A wrapper type for a full, complete transaction (i.e. with blob sidecars attached). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FullTransaction { + pub tx: PooledTransactionsElement, + pub sender: Option
, +} + +impl From for FullTransaction { + fn from(tx: PooledTransactionsElement) -> Self { + Self { tx, sender: None } + } +} + +impl std::ops::Deref for FullTransaction { + type Target = PooledTransactionsElement; + + fn deref(&self) -> &Self::Target { + &self.tx + } +} + +impl std::ops::DerefMut for FullTransaction { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.tx + } +} + +impl FullTransaction { + /// Convenience method to parse a raw transaction into a `FullTransaction`. + pub fn decode_enveloped(data: impl AsRef<[u8]>) -> eyre::Result { + let tx = PooledTransactionsElement::decode_enveloped(&mut data.as_ref())?; + Ok(Self { tx, sender: None }) + } + + pub fn into_inner(self) -> PooledTransactionsElement { + self.tx + } + + /// Returns the sender of the transaction, if recovered. + pub fn sender(&self) -> Option<&Address> { + self.sender.as_ref() + } + + /// Returns the effective miner gas tip cap (`gasTipCap`) for the given base fee: + /// `min(maxFeePerGas - baseFee, maxPriorityFeePerGas)` + /// + /// Returns `None` if the basefee is higher than the [`Transaction::max_fee_per_gas`]. + /// Ref: https://github.com/paradigmxyz/reth/blob/2d592125128c3742ff97b321884f93f9063abcb2/crates/primitives/src/transaction/mod.rs#L444 + pub fn effective_tip_per_gas(&self, base_fee: u128) -> Option { + let max_fee_per_gas = self.max_fee_per_gas(); + + if max_fee_per_gas < base_fee { + return None; + } + + // Calculate the difference between max_fee_per_gas and base_fee + let fee = max_fee_per_gas - base_fee; + + // Compare the fee with max_priority_fee_per_gas (or gas price for non-EIP1559 transactions) + if let Some(priority_fee) = self.max_priority_fee_per_gas() { + Some(fee.min(priority_fee)) + } else { + Some(fee) + } + } +} + +/// Serialize a list of transactions into a sequence of hex-encoded strings. +pub fn serialize_txs( + txs: &[FullTransaction], + serializer: S, +) -> Result { + let mut seq = serializer.serialize_seq(Some(txs.len()))?; + for tx in txs { + let encoded = tx.tx.envelope_encoded(); + seq.serialize_element(&format!("0x{}", hex::encode(encoded)))?; + } + seq.end() +} + +/// Deserialize a list of transactions from a sequence of hex-encoded strings. +pub fn deserialize_txs<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let hex_strings = > as de::Deserialize>::deserialize(deserializer)?; + let mut txs = Vec::with_capacity(hex_strings.len()); + + for s in hex_strings { + let data = hex::decode(s.trim_start_matches("0x")).map_err(de::Error::custom)?; + let tx = PooledTransactionsElement::decode_enveloped(&mut data.as_slice()) + .map_err(de::Error::custom) + .map(|tx| FullTransaction { tx, sender: None })?; + txs.push(tx); + } + + Ok(txs) +} diff --git a/bolt-sidecar/src/signer/commit_boost.rs b/bolt-sidecar/src/signer/commit_boost.rs index 1757e8071..d3c81a164 100644 --- a/bolt-sidecar/src/signer/commit_boost.rs +++ b/bolt-sidecar/src/signer/commit_boost.rs @@ -40,7 +40,7 @@ pub enum CommitBoostError { #[allow(unused)] impl CommitBoostSigner { /// Create a new [CommitBoostSigner] instance - pub async fn new(signer_server_address: String, jwt: &str) -> SignerResult { + pub fn new(signer_server_address: String, jwt: &str) -> SignerResult { let signer_client = SignerClient::new(signer_server_address, jwt).map_err(CommitBoostError::Other)?; @@ -178,7 +178,7 @@ mod test { return Ok(()); } }; - let signer = CommitBoostSigner::new(signer_server_address, &jwt_hex).await.unwrap(); + let signer = CommitBoostSigner::new(signer_server_address, &jwt_hex).unwrap(); // Generate random data for the test let mut rng = rand::thread_rng(); @@ -208,7 +208,7 @@ mod test { return Ok(()); } }; - let signer = CommitBoostSigner::new(signer_server_address, &jwt_hex).await.unwrap(); + let signer = CommitBoostSigner::new(signer_server_address, &jwt_hex).unwrap(); let pubkey = signer.get_proxy_ecdsa_pubkey(); // Generate random data for the test diff --git a/bolt-sidecar/src/signer/keystore.rs b/bolt-sidecar/src/signer/keystore.rs index f0dda8a6d..aaf4441bd 100644 --- a/bolt-sidecar/src/signer/keystore.rs +++ b/bolt-sidecar/src/signer/keystore.rs @@ -1,6 +1,7 @@ //! An ERC-2335 keystore signer. use std::{ + collections::HashSet, ffi::OsString, fmt::Debug, fs::{self, DirEntry, ReadDir}, @@ -10,6 +11,7 @@ use std::{ use alloy::rpc::types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN; +use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use lighthouse_bls::Keypair; use lighthouse_eth2_keystore::Keystore; use ssz::Encode; @@ -41,6 +43,7 @@ pub struct KeystoreSigner { } impl KeystoreSigner { + /// Creates a new `KeystoreSigner` from the keystore files in the `keys_path` directory. pub fn new(keys_path: Option<&str>, password: &[u8], chain: ChainConfig) -> SignerResult { let keystores_paths = keystore_paths(keys_path)?; let mut keypairs = Vec::with_capacity(keystores_paths.len()); @@ -57,6 +60,17 @@ impl KeystoreSigner { Ok(Self { keypairs, chain }) } + /// Returns the public keys of the keypairs in the keystore. + pub fn pubkeys(&self) -> HashSet { + self.keypairs + .iter() + .map(|kp| { + BlsPublicKey::try_from(kp.pk.serialize().to_vec().as_ref()).expect("valid pubkey") + }) + .collect::>() + } + + /// Signs a message with the keystore signer and the Commit Boost domain pub fn sign_commit_boost_root( &self, root: [u8; 32], @@ -65,6 +79,7 @@ impl KeystoreSigner { self.sign_root(root, public_key, self.chain.commit_boost_domain()) } + /// Signs a message with the keystore signer. fn sign_root( &self, root: [u8; 32], diff --git a/bolt-sidecar/src/signer/mod.rs b/bolt-sidecar/src/signer/mod.rs index 3d74c2629..c9ae92a1a 100644 --- a/bolt-sidecar/src/signer/mod.rs +++ b/bolt-sidecar/src/signer/mod.rs @@ -1,3 +1,7 @@ +use std::collections::HashSet; + +use ethereum_consensus::crypto::bls::PublicKey as BlsPublicKey; + pub mod commit_boost; use commit_boost::CommitBoostSigner; @@ -7,17 +11,6 @@ use keystore::KeystoreSigner; pub mod local; use local::LocalSigner; -/// Signer for BLS signatures. -#[derive(Debug, Clone)] -pub enum SignerBLS { - /// Local signer with a BLS secret key. - Local(LocalSigner), - /// Signer from Commit-Boost. - CommitBoost(CommitBoostSigner), - /// Signer consisting of multiple keypairs loaded from ERC-2335 keystores files. - Keystore(KeystoreSigner), -} - /// Error in the signer. #[derive(Debug, thiserror::Error)] pub enum SignerError { @@ -30,3 +23,25 @@ pub enum SignerError { } pub type SignerResult = std::result::Result; + +/// Signer for BLS signatures. +#[derive(Debug, Clone)] +pub enum SignerBLS { + /// Local signer with a BLS secret key. + Local(LocalSigner), + /// Signer from Commit-Boost. + CommitBoost(CommitBoostSigner), + /// Signer consisting of multiple keypairs loaded from ERC-2335 keystores files. + Keystore(KeystoreSigner), +} + +impl SignerBLS { + /// Returns all the public keys available for signing. + pub fn available_pubkeys(&self) -> HashSet { + match self { + SignerBLS::Local(signer) => [signer.pubkey()].into(), + SignerBLS::CommitBoost(signer) => [signer.pubkey()].into(), + SignerBLS::Keystore(signer) => signer.pubkeys(), + } + } +} diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 29073c299..b82418317 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -74,7 +74,7 @@ pub enum ValidationError { MaxCommittedGasReachedForSlot(u64, u64), /// The signature is invalid. #[error("Invalid signature")] - Signature(#[from] crate::primitives::SignatureError), + Signature(#[from] crate::primitives::commitment::SignatureError), /// Could not recover signature, #[error("Could not recover signer")] RecoverSigner, diff --git a/bolt-sidecar/src/telemetry/metrics.rs b/bolt-sidecar/src/telemetry/metrics.rs index ca6699d7f..23751b072 100644 --- a/bolt-sidecar/src/telemetry/metrics.rs +++ b/bolt-sidecar/src/telemetry/metrics.rs @@ -3,7 +3,7 @@ use std::time::Duration; use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram}; use reth_primitives::TxType; -use crate::primitives::tx_type_str; +use crate::primitives::transaction::tx_type_str; // Counters ---------------------------------------------------------------- /// Counter for the total number of HTTP requests received. diff --git a/bolt-sidecar/test_data/delegations.json b/bolt-sidecar/test_data/delegations.json new file mode 100644 index 000000000..856d75cc5 --- /dev/null +++ b/bolt-sidecar/test_data/delegations.json @@ -0,0 +1,10 @@ +[ + { + "message": { + "action": 0, + "validator_pubkey": "0x83b85769a8f2a1a6bd3a609e51b460f6fb897daff1157991479421493926faeffa6670152524403929a8a7e551d345f3", + "delegatee_pubkey": "0x8d0edf4fe9c80cd640220ca7a68a48efcbc56a13536d6b274bf3719befaffa13688ebee9f37414b3dddc8c7e77233ce8" + }, + "signature": "0x8dc3f4ea5584fcfecd26e16f9d43789d59a66cfb0860ef88ac2a3e7c6a4054c973c0478809db747c821a8a10e672902012e8dd1830a059a30ec41025d57afa3d5408008a68eca8b1bc2e6fc878c41207accb2df2a3af30f8c64af98006c43ca1" + } +]