diff --git a/.gitignore b/.gitignore index be2b1a284..da4d6e6a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *_dump.log target/ +.vscode +.idea +.DS_Store +.env diff --git a/bolt-boost/src/server.rs b/bolt-boost/src/server.rs index 444cc4c5d..a0022b551 100644 --- a/bolt-boost/src/server.rs +++ b/bolt-boost/src/server.rs @@ -25,7 +25,9 @@ use cb_common::{ config::PbsConfig, constants::APPLICATION_BUILDER_DOMAIN, pbs::{ - error::{PbsError, ValidationError}, GetHeaderResponse, RelayClient, SignedExecutionPayloadHeader, EMPTY_TX_ROOT_HASH, HEADER_SLOT_UUID_KEY, HEADER_START_TIME_UNIX_MS + error::{PbsError, ValidationError}, + GetHeaderResponse, RelayClient, SignedExecutionPayloadHeader, EMPTY_TX_ROOT_HASH, + HEADER_SLOT_UUID_KEY, HEADER_START_TIME_UNIX_MS, }, signature::verify_signed_message, types::Chain, diff --git a/bolt-sidecar/src/builder/mod.rs b/bolt-sidecar/src/builder/mod.rs index 7491ef803..88f59b3d3 100644 --- a/bolt-sidecar/src/builder/mod.rs +++ b/bolt-sidecar/src/builder/mod.rs @@ -83,7 +83,7 @@ impl LocalBuilder { payload_and_bid: None, fallback_builder: FallbackPayloadBuilder::new(config, beacon_api_client, genesis_time), secret_key: config.builder_private_key.clone(), - chain: config.chain.clone(), + chain: config.chain, } } diff --git a/bolt-sidecar/src/client/commit_boost.rs b/bolt-sidecar/src/client/commit_boost.rs index a95ecc0fa..ad5931727 100644 --- a/bolt-sidecar/src/client/commit_boost.rs +++ b/bolt-sidecar/src/client/commit_boost.rs @@ -109,11 +109,11 @@ impl CommitBoostSigner { #[async_trait::async_trait] impl SignerBLS for CommitBoostSigner { - async fn sign(&self, data: &[u8; 32]) -> eyre::Result { - let request = SignConsensusRequest::builder( - *self.pubkeys.read().first().expect("consensus pubkey loaded"), - ) - .with_msg(data); + async fn sign_commit_boost_root(&self, data: &[u8; 32]) -> eyre::Result { + let request = SignConsensusRequest { + pubkey: *self.pubkeys.read().first().expect("consensus pubkey loaded"), + object_root: *data, + }; debug!(?request, "Requesting signature from commit_boost"); @@ -167,7 +167,7 @@ mod test { let mut data = [0u8; 32]; rng.fill(&mut data); - let signature = signer.sign(&data).await.unwrap(); + let signature = signer.sign_commit_boost_root(&data).await.unwrap(); let sig = blst::min_pk::Signature::from_bytes(signature.as_ref()).unwrap(); let pubkey = signer.get_consensus_pubkey(); let bls_pubkey = blst::min_pk::PublicKey::from_bytes(pubkey.as_ref()).unwrap(); diff --git a/bolt-sidecar/src/config/chain.rs b/bolt-sidecar/src/config/chain.rs index 9aff45db9..0c5dda74e 100644 --- a/bolt-sidecar/src/config/chain.rs +++ b/bolt-sidecar/src/config/chain.rs @@ -1,7 +1,8 @@ -use alloy::primitives::b256; -use clap::{Args, ValueEnum}; use std::time::Duration; +use clap::{Args, ValueEnum}; +use ethereum_consensus::deneb::{compute_fork_data_root, Root}; + /// Default commitment deadline duration. /// /// The sidecar will stop accepting new commitments for the next block @@ -12,25 +13,15 @@ pub const DEFAULT_COMMITMENT_DEADLINE_IN_MILLIS: u64 = 8_000; /// Default slot time duration in seconds. pub const DEFAULT_SLOT_TIME_IN_SECONDS: u64 = 12; -/// Builder domain for signing messages on Ethereum Mainnet. -const BUILDER_DOMAIN_MAINNET: [u8; 32] = - b256!("00000001f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a9").0; - -/// Builder domain for signing messages on Holesky. -const BUILDER_DOMAIN_HOLESKY: [u8; 32] = - b256!("000000015b83a23759c560b2d0c64576e1dcfc34ea94c4988f3e0d9f77f05387").0; +/// The domain mask for signing application-builder messages. +pub const APPLICATION_BUILDER_DOMAIN_MASK: [u8; 4] = [0, 0, 0, 1]; -/// Builder domain for signing messages on stock Kurtosis devnets. -const BUILDER_DOMAIN_KURTOSIS: [u8; 32] = - b256!("000000010b41be4cdb34d183dddca5398337626dcdcfaf1720c1202d3b95f84e").0; - -/// Builder domain for signing messages on Helder. -const BUILDER_DOMAIN_HELDER: [u8; 32] = - b256!("0000000194c41af484fff7964969e0bdd922f82dff0f4be87a60d0664cc9d1ff").0; +/// The domain mask for signing commit-boost messages. +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, Args)] +#[derive(Debug, Clone, Copy, Args)] pub struct ChainConfig { /// Chain on which the sidecar is running #[clap(long, env = "BOLT_SIDECAR_CHAIN", default_value = "mainnet")] @@ -64,7 +55,7 @@ impl Default for ChainConfig { } /// Supported chains for the sidecar -#[derive(Debug, Clone, ValueEnum)] +#[derive(Debug, Clone, Copy, ValueEnum)] #[clap(rename_all = "kebab_case")] #[allow(missing_docs)] pub enum Chain { @@ -100,20 +91,20 @@ impl ChainConfig { self.slot_time } - /// Get the domain for signing messages on the given chain. + /// Get the domain for signing application-builder messages on the given chain. pub fn builder_domain(&self) -> [u8; 32] { - match self.chain { - Chain::Mainnet => BUILDER_DOMAIN_MAINNET, - Chain::Holesky => BUILDER_DOMAIN_HOLESKY, - Chain::Helder => BUILDER_DOMAIN_HELDER, - Chain::Kurtosis => BUILDER_DOMAIN_KURTOSIS, - } + self.compute_domain_from_mask(APPLICATION_BUILDER_DOMAIN_MASK) + } + + /// Get the domain for signing commit-boost messages on the given chain. + pub fn commit_boost_domain(&self) -> [u8; 32] { + self.compute_domain_from_mask(COMMIT_BOOST_DOMAIN_MASK) } /// Get the fork version for the given chain. pub fn fork_version(&self) -> [u8; 4] { match self.chain { - Chain::Mainnet => [0u8; 4], + Chain::Mainnet => [0, 0, 0, 0], Chain::Holesky => [1, 1, 112, 0], Chain::Helder => [16, 0, 0, 0], Chain::Kurtosis => [16, 0, 0, 56], @@ -124,6 +115,23 @@ impl ChainConfig { pub fn commitment_deadline(&self) -> Duration { Duration::from_millis(self.commitment_deadline) } + + /// Compute the domain for signing messages on the given chain. + fn compute_domain_from_mask(&self, mask: [u8; 4]) -> [u8; 32] { + let mut domain = [0; 32]; + + let fork_version = self.fork_version(); + + // Note: the application builder domain specs require the genesis_validators_root + // to be 0x00 for any out-of-protocol message. The commit-boost domain follows the + // same rule. + let root = Root::default(); + let fork_data_root = compute_fork_data_root(fork_version, root).expect("valid fork data"); + + domain[..4].copy_from_slice(&mask); + domain[4..].copy_from_slice(&fork_data_root[..28]); + domain + } } #[cfg(test)] @@ -144,3 +152,37 @@ impl ChainConfig { Self { chain: Chain::Kurtosis, slot_time: slot_time_in_seconds, commitment_deadline } } } + +#[cfg(test)] +mod tests { + use alloy::primitives::b256; + + const BUILDER_DOMAIN_MAINNET: [u8; 32] = + b256!("00000001f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a9").0; + + const BUILDER_DOMAIN_HOLESKY: [u8; 32] = + b256!("000000015b83a23759c560b2d0c64576e1dcfc34ea94c4988f3e0d9f77f05387").0; + + const BUILDER_DOMAIN_HELDER: [u8; 32] = + b256!("0000000194c41af484fff7964969e0bdd922f82dff0f4be87a60d0664cc9d1ff").0; + + const BUILDER_DOMAIN_KURTOSIS: [u8; 32] = + b256!("000000010b41be4cdb34d183dddca5398337626dcdcfaf1720c1202d3b95f84e").0; + + #[test] + fn test_compute_builder_domains() { + use super::ChainConfig; + + let mainnet = ChainConfig::mainnet(); + assert_eq!(mainnet.builder_domain(), BUILDER_DOMAIN_MAINNET); + + let holesky = ChainConfig::holesky(); + assert_eq!(holesky.builder_domain(), BUILDER_DOMAIN_HOLESKY); + + let helder = ChainConfig::helder(); + assert_eq!(helder.builder_domain(), BUILDER_DOMAIN_HELDER); + + let kurtosis = ChainConfig::kurtosis(0, 0); + assert_eq!(kurtosis.builder_domain(), BUILDER_DOMAIN_KURTOSIS); + } +} diff --git a/bolt-sidecar/src/crypto/bls.rs b/bolt-sidecar/src/crypto/bls.rs index 618f113d4..56179aba5 100644 --- a/bolt-sidecar/src/crypto/bls.rs +++ b/bolt-sidecar/src/crypto/bls.rs @@ -2,11 +2,14 @@ use std::fmt::Debug; use alloy::primitives::FixedBytes; use blst::{min_pk::Signature, BLST_ERROR}; +use ethereum_consensus::deneb::compute_signing_root; use rand::RngCore; pub use blst::min_pk::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}; pub use ethereum_consensus::deneb::BlsSignature; +use crate::ChainConfig; + /// The BLS Domain Separator used in Ethereum 2.0. pub const BLS_DST_PREFIX: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; @@ -18,65 +21,108 @@ pub type BLSSig = FixedBytes<96>; pub trait SignableBLS { /// Returns the digest of the object. fn digest(&self) -> [u8; 32]; - - /// Sign the object with the given key. Returns the signature. - /// - /// Note: The default implementation should be used where possible. - #[allow(dead_code)] - fn sign(&self, key: &BlsSecretKey) -> Signature { - sign_with_prefix(key, &self.digest()) - } - - /// Verify the signature of the object with the given public key. - /// - /// Note: The default implementation should be used where possible. - fn verify(&self, signature: &Signature, pubkey: &BlsPublicKey) -> bool { - signature.verify(false, &self.digest(), BLS_DST_PREFIX, &[], pubkey, true) == - BLST_ERROR::BLST_SUCCESS - } } /// A generic signing trait to generate BLS signatures. +/// +/// Note: we keep this async to allow remote signer implementations. #[async_trait::async_trait] pub trait SignerBLS: Send + Debug { /// Sign the given data and return the signature. - async fn sign(&self, data: &[u8; 32]) -> eyre::Result; + async fn sign_commit_boost_root(&self, data: &[u8; 32]) -> eyre::Result; } -/// A BLS signer that can sign any type that implements the `Signable` trait. -#[derive(Debug, Clone)] +/// A BLS signer that can sign any type that implements the [`SignableBLS`] trait. +#[derive(Clone)] pub struct Signer { + chain: ChainConfig, key: BlsSecretKey, } +impl Debug for Signer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signer") + .field("pubkey", &self.pubkey()) + .field("chain", &self.chain.name()) + .finish() + } +} + impl Signer { /// Create a new signer with the given BLS secret key. - pub fn new(key: BlsSecretKey) -> Self { - Self { key } + pub fn new(key: BlsSecretKey, chain: ChainConfig) -> Self { + Self { key, chain } } - /// Create a signer with a random BLS key. + /// Create a signer with a random BLS key configured for Mainnet for testing. + #[cfg(test)] pub fn random() -> Self { - Self { key: random_bls_secret() } + Self { key: random_bls_secret(), chain: ChainConfig::mainnet() } + } + + /// Get the public key of the signer. + pub fn pubkey(&self) -> BlsPublicKey { + self.key.sk_to_pk() + } + + /// Sign an SSZ object root with the Application Builder domain. + pub fn sign_application_builder_root(&self, root: [u8; 32]) -> eyre::Result { + self.sign_root(root, self.chain.builder_domain()) + } + + /// Sign an SSZ object root with the Commit Boost domain. + pub fn sign_commit_boost_root(&self, root: [u8; 32]) -> eyre::Result { + self.sign_root(root, self.chain.commit_boost_domain()) + } + + /// Sign an SSZ object root with the given domain. + pub fn sign_root(&self, root: [u8; 32], domain: [u8; 32]) -> eyre::Result { + let signing_root = compute_signing_root(&root, domain)?; + let sig = self.key.sign(signing_root.as_slice(), BLS_DST_PREFIX, &[]); + Ok(BLSSig::from_slice(&sig.to_bytes())) + } + + /// Verify the signature with the public key of the signer using the Application Builder domain. + pub fn verify_application_builder_root( + &self, + root: [u8; 32], + signature: &Signature, + ) -> eyre::Result<()> { + self.verify_root(root, signature, &self.pubkey(), self.chain.builder_domain()) + } + + /// Verify the signature with the public key of the signer using the Commit Boost domain. + pub fn verify_commit_boost_root( + &self, + root: [u8; 32], + signature: &Signature, + ) -> eyre::Result<()> { + self.verify_root(root, signature, &self.pubkey(), self.chain.commit_boost_domain()) } /// Verify the signature of the object with the given public key. - #[allow(dead_code)] - pub fn verify( + pub fn verify_root( &self, - obj: &T, + root: [u8; 32], signature: &Signature, pubkey: &BlsPublicKey, - ) -> bool { - obj.verify(signature, pubkey) + domain: [u8; 32], + ) -> eyre::Result<()> { + let signing_root = compute_signing_root(&root, domain)?; + + let res = signature.verify(true, signing_root.as_ref(), BLS_DST_PREFIX, &[], pubkey, true); + if res == BLST_ERROR::BLST_SUCCESS { + Ok(()) + } else { + eyre::bail!(format!("Invalid signature: {:?}", res)) + } } } #[async_trait::async_trait] impl SignerBLS for Signer { - async fn sign(&self, data: &[u8; 32]) -> eyre::Result { - let sig = sign_with_prefix(&self.key, data); - Ok(BLSSig::from(sig.to_bytes())) + async fn sign_commit_boost_root(&self, data: &[u8; 32]) -> eyre::Result { + self.sign_commit_boost_root(*data) } } @@ -93,26 +139,18 @@ pub fn random_bls_secret() -> BlsSecretKey { BlsSecretKey::key_gen(&ikm, &[]).unwrap() } -/// Sign the given data with the given BLS secret key. -#[inline] -fn sign_with_prefix(key: &BlsSecretKey, data: &[u8]) -> Signature { - key.sign(data, BLS_DST_PREFIX, &[]) -} - #[cfg(test)] mod tests { use crate::{ - crypto::bls::{SignableBLS, Signer, SignerBLS}, - test_util::{test_bls_secret_key, TestSignableData}, + crypto::bls::{SignableBLS, Signer}, + test_util::TestSignableData, }; use rand::Rng; #[tokio::test] async fn test_bls_signer() { - let key = test_bls_secret_key(); - let pubkey = key.sk_to_pk(); - let signer = Signer::new(key); + let signer = Signer::random(); // Generate random data for the test let mut rng = rand::thread_rng(); @@ -120,8 +158,8 @@ mod tests { rng.fill(&mut data); let msg = TestSignableData { data }; - let signature = SignerBLS::sign(&signer, &msg.digest()).await.unwrap(); + let signature = signer.sign_commit_boost_root(msg.digest()).unwrap(); let sig = blst::min_pk::Signature::from_bytes(signature.as_ref()).unwrap(); - assert!(signer.verify(&msg, &sig, &pubkey)); + assert!(signer.verify_commit_boost_root(msg.digest(), &sig).is_ok()); } } diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index 6d9d1daae..bee3969ed 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -65,7 +65,8 @@ impl SidecarDriver { let state_client = StateClient::new(cfg.execution_api_url.clone()); // Constraints are signed with a BLS private key - let constraint_signer = BlsSigner::new(cfg.private_key.clone().unwrap()); + let constraint_signing_key = cfg.private_key.clone().expect("Private key must be provided"); + let constraint_signer = BlsSigner::new(constraint_signing_key, cfg.chain); // Commitment responses are signed with a regular Ethereum wallet private key. // This is now generated randomly because slashing is not yet implemented. @@ -221,21 +222,23 @@ impl SidecarDriver SignedConstraints { message, signature }, - Err(err) => { - error!(?err, "Failed to sign constraints"); - let _ = response.send(Err(CommitmentError::Internal)); - return; - } - }; + let signed_constraints = + match self.constraint_signer.sign_commit_boost_root(&message.digest()).await { + Ok(signature) => SignedConstraints { message, signature }, + Err(err) => { + error!(?err, "Failed to sign constraints"); + let _ = response.send(Err(CommitmentError::Internal)); + return; + } + }; ApiMetrics::increment_transactions_preconfirmed(tx_type); self.execution.add_constraint(slot, signed_constraints); diff --git a/bolt-sidecar/src/primitives/constraint.rs b/bolt-sidecar/src/primitives/constraint.rs index 6f65011f5..a52a460b2 100644 --- a/bolt-sidecar/src/primitives/constraint.rs +++ b/bolt-sidecar/src/primitives/constraint.rs @@ -1,36 +1,14 @@ -use alloy::{ - primitives::keccak256, - signers::k256::sha2::{Digest, Sha256}, -}; +use alloy::signers::k256::sha2::{Digest, Sha256}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; -use secp256k1::Message; use serde::{Deserialize, Serialize}; use crate::{ - crypto::{bls::BLSSig, ecdsa::SignableECDSA, SignableBLS}, + crypto::{bls::BLSSig, SignableBLS}, primitives::{deserialize_txs, serialize_txs}, }; use super::{FullTransaction, InclusionRequest}; -/// What the proposer sidecar will need to sign to confirm the inclusion request. -impl SignableECDSA for ConstraintsMessage { - fn digest(&self) -> Message { - let mut data = Vec::new(); - data.extend_from_slice(&self.pubkey.to_vec()); - data.extend_from_slice(&self.slot.to_le_bytes()); - - let mut constraint_bytes = Vec::new(); - for constraint in &self.transactions { - constraint_bytes.extend_from_slice(&constraint.envelope_encoded().0); - } - data.extend_from_slice(&constraint_bytes); - - let hash = keccak256(data).0; - Message::from_digest_slice(&hash).expect("digest") - } -} - /// The inclusion request transformed into an explicit list of signed constraints /// that need to be forwarded to the PBS pipeline to inform block production. pub type BatchedSignedConstraints = Vec; diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 373fa39fa..a0dbc4ae6 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -542,7 +542,7 @@ mod tests { use reth_primitives::constants::GWEI_TO_WEI; use crate::{ - crypto::{bls::Signer, SignableBLS, SignerBLS}, + crypto::{bls::Signer, SignableBLS}, primitives::{ConstraintsMessage, SignedConstraints}, state::fetcher, test_util::{create_signed_commitment_request, default_test_transaction, launch_anvil}, @@ -736,7 +736,7 @@ mod tests { Default::default(), request.as_inclusion_request().unwrap().clone(), ); - let signature = signer.sign(&message.digest()).await?; + let signature = signer.sign_commit_boost_root(message.digest())?; let signed_constraints = SignedConstraints { message, signature }; state.add_constraint(10, signed_constraints); @@ -987,7 +987,7 @@ mod tests { let bls_signer = Signer::random(); let message = ConstraintsMessage::build(Default::default(), inclusion_request); - let signature = bls_signer.sign(&message.digest()).await.unwrap(); + let signature = bls_signer.sign_commit_boost_root(message.digest()).unwrap(); let signed_constraints = SignedConstraints { message, signature }; state.add_constraint(target_slot, signed_constraints); @@ -1035,7 +1035,7 @@ mod tests { let bls_signer = Signer::random(); let message = ConstraintsMessage::build(Default::default(), inclusion_request); - let signature = bls_signer.sign(&message.digest()).await.unwrap(); + let signature = bls_signer.sign_commit_boost_root(message.digest()).unwrap(); let signed_constraints = SignedConstraints { message, signature }; state.add_constraint(target_slot, signed_constraints); @@ -1082,7 +1082,7 @@ mod tests { let bls_signer = Signer::random(); let message = ConstraintsMessage::build(Default::default(), inclusion_request); - let signature = bls_signer.sign(&message.digest()).await.unwrap(); + let signature = bls_signer.sign_commit_boost_root(message.digest()).unwrap(); let signed_constraints = SignedConstraints { message, signature }; state.add_constraint(target_slot, signed_constraints); diff --git a/bolt-sidecar/src/test_util.rs b/bolt-sidecar/src/test_util.rs index f954e97d8..d62429434 100644 --- a/bolt-sidecar/src/test_util.rs +++ b/bolt-sidecar/src/test_util.rs @@ -18,7 +18,7 @@ use secp256k1::Message; use tracing::warn; use crate::{ - crypto::{bls::Signer as BlsSigner, ecdsa::SignableECDSA, SignableBLS, SignerBLS}, + crypto::{bls::Signer as BlsSigner, ecdsa::SignableECDSA, SignableBLS}, primitives::{ CommitmentRequest, ConstraintsMessage, DelegationMessage, FullTransaction, InclusionRequest, RevocationMessage, SignedConstraints, SignedDelegation, SignedRevocation, @@ -116,11 +116,6 @@ pub(crate) fn default_test_transaction(sender: Address, nonce: Option) -> T .with_max_fee_per_gas(20_000_000_000) } -/// Create a default BLS secret key -pub(crate) fn test_bls_secret_key() -> SecretKey { - SecretKey::key_gen(&[0u8; 32], &[]).unwrap() -} - /// Arbitrary bytes that can be signed with both ECDSA and BLS keys pub(crate) struct TestSignableData { pub data: [u8; 32], @@ -194,9 +189,8 @@ fn random_constraints(count: usize) -> Vec { #[tokio::test] async fn generate_test_data() { - let sk = test_bls_secret_key(); - let pk = sk.sk_to_pk(); - let signer = BlsSigner::new(sk); + let signer = BlsSigner::random(); + let pk = signer.pubkey(); println!("Validator Public Key: {}", hex::encode(pk.to_bytes())); @@ -217,7 +211,7 @@ async fn generate_test_data() { let digest = SignableBLS::digest(&delegation_msg); // Sign the Delegation message - let delegation_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + let delegation_signature = signer.sign_commit_boost_root(digest).unwrap(); // Create SignedDelegation let signed_delegation = SignedDelegation { @@ -240,7 +234,7 @@ async fn generate_test_data() { let digest = SignableBLS::digest(&revocation_msg); // Sign the Revocation message - let revocation_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + let revocation_signature = signer.sign_commit_boost_root(digest).unwrap(); // Create SignedRevocation let signed_revocation = SignedRevocation { @@ -266,7 +260,7 @@ async fn generate_test_data() { let digest = SignableBLS::digest(&constraints_msg); // Sign the ConstraintsMessage - let constraints_signature = SignerBLS::sign(&signer, &digest).await.unwrap(); + let constraints_signature = signer.sign_commit_boost_root(digest).unwrap(); // Create SignedConstraints let signed_constraints =