diff --git a/bolt-sidecar/src/client/constraints_client.rs b/bolt-sidecar/src/client/constraints_client.rs index db0375c1a..cb15c2c2a 100644 --- a/bolt-sidecar/src/client/constraints_client.rs +++ b/bolt-sidecar/src/client/constraints_client.rs @@ -5,7 +5,8 @@ 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; @@ -48,6 +49,15 @@ impl ConstraintsClient { self.delegations.extend(delegations); } + /// Finds all delegations for the given public key. + pub fn find_delegatees(&self, pubkey: &BlsPublicKey) -> Vec<&BlsPublicKey> { + self.delegations + .iter() + .filter(|d| d.message.delegatee_pubkey == *pubkey) + .map(|d| &d.message.delegatee_pubkey) + .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); diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index 4fb806741..de2a023ed 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -33,15 +33,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, @@ -110,20 +127,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 } } @@ -237,7 +247,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))); @@ -262,13 +272,28 @@ impl SidecarDriver { "Validation against execution state passed" ); - // parse the request into constraints and sign them - let slot = inclusion_request.slot; - - let pubkey = match self.constraint_signer { - SignerBLS::Local(ref signer) => signer.pubkey(), - SignerBLS::CommitBoost(ref signer) => signer.pubkey(), - SignerBLS::Keystore(_) => validator_pubkey.clone(), + let delegatees = self.constraints_client.find_delegatees(&validator_pubkey); + let available_pubkeys = self.constraint_signer.available_pubkeys(); + + // 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. + let pubkey = if delegatees.is_empty() { + if available_pubkeys.contains(&validator_pubkey) { + validator_pubkey.clone() + } else { + error!(%target_slot, %validator_pubkey, "No authorized private key available to sign constraints"); + let _ = response.send(Err(CommitmentError::Internal)); + return; + } + } else if let Some(delegatee) = available_pubkeys.iter().find(|k| delegatees.contains(k)) { + delegatee.clone() + } else { + error!(%target_slot, "No delegatee key found, unable to sign constraints"); + let _ = response.send(Err(CommitmentError::Internal)); + return; }; // NOTE: we iterate over the transactions in the request and generate a signed constraint @@ -277,7 +302,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 { @@ -298,12 +323,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) => { + info!(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() 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..4814cf244 100644 --- a/bolt-sidecar/src/signer/keystore.rs +++ b/bolt-sidecar/src/signer/keystore.rs @@ -10,6 +10,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 +42,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 +59,17 @@ impl KeystoreSigner { Ok(Self { keypairs, chain }) } + /// Returns the public keys of the keypairs in the keystore. + pub fn pubkeys(&self) -> Vec { + 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 +78,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..dced3506e 100644 --- a/bolt-sidecar/src/signer/mod.rs +++ b/bolt-sidecar/src/signer/mod.rs @@ -1,3 +1,5 @@ +use ethereum_consensus::crypto::bls::PublicKey as BlsPublicKey; + pub mod commit_boost; use commit_boost::CommitBoostSigner; @@ -7,17 +9,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 +21,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) -> Vec { + match self { + SignerBLS::Local(signer) => vec![signer.pubkey()], + SignerBLS::CommitBoost(signer) => vec![signer.pubkey()], + SignerBLS::Keystore(signer) => signer.pubkeys(), + } + } +}