diff --git a/api/signer-api.yml b/api/signer-api.yml index a6c427a4..a6edd6bb 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -79,6 +79,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -217,6 +219,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -355,6 +359,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: proxy: "0x71f65e9f6336770e22d148bd5e89b391a1c3b0bb" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -629,9 +635,15 @@ components: object_root: description: The 32-byte data that was signed, with `0x` prefix $ref: "#/components/schemas/B256" - signing_id: + module_signing_id: description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" + chain_id: + description: The chain ID that the signature is valid for, as specified in the Commit-Boost configuration + type: integer + example: 1 signature: description: The BLS signature of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). $ref: "#/components/schemas/BlsSignature" @@ -647,6 +659,18 @@ components: module_signing_id: description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" + chain_id: + description: The chain ID that the signature is valid for, as specified in the Commit-Boost configuration + type: integer + example: 1 signature: description: The ECDSA signature (in Ethereum RSV format) of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). - $ref: "#/components/schemas/EcdsaSignature" \ No newline at end of file + $ref: "#/components/schemas/EcdsaSignature" + Nonce: + type: integer + description: If your module tracks nonces per signature (e.g., to prevent replay attacks), this is the unique nonce to use for the signature. It should be an unsigned 64-bit integer in big-endian format. It must be between 0 and 2^64-2, inclusive. If your module doesn't use nonces, we suggest setting this to 2^64-1 instead of 0 because 0 is a legal nonce and will cause complications with your module if you ever want to use a nonce in the future. + minimum: 0 + maximum: 18446744073709551614 // 2^64-2 + example: 1 diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 5e101092..81edd5fe 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -77,15 +77,16 @@ impl fmt::Display for SignedProxyDelegation { pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, pub object_root: B256, + pub nonce: u64, } impl SignConsensusRequest { - pub fn new(pubkey: BlsPublicKey, object_root: B256) -> Self { - Self { pubkey, object_root } + pub fn new(pubkey: BlsPublicKey, object_root: B256, nonce: u64) -> Self { + Self { pubkey, object_root, nonce } } pub fn builder(pubkey: BlsPublicKey) -> Self { - Self::new(pubkey, B256::ZERO) + Self::new(pubkey, B256::ZERO, u64::MAX - 1) } pub fn with_root>(self, object_root: R) -> Self { @@ -95,15 +96,20 @@ impl SignConsensusRequest { pub fn with_msg(self, msg: &impl TreeHash) -> Self { self.with_root(msg.tree_hash_root().0) } + + pub fn with_nonce(self, nonce: u64) -> Self { + Self { nonce, ..self } + } } impl Display for SignConsensusRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "Consensus(pubkey: {}, object_root: {})", + "Consensus(pubkey: {}, object_root: {}, nonce: {})", self.pubkey, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } @@ -112,15 +118,16 @@ impl Display for SignConsensusRequest { pub struct SignProxyRequest { pub proxy: T, pub object_root: B256, + pub nonce: u64, } impl SignProxyRequest { - pub fn new(proxy: T, object_root: B256) -> Self { - Self { proxy, object_root } + pub fn new(proxy: T, object_root: B256, nonce: u64) -> Self { + Self { proxy, object_root, nonce } } pub fn builder(proxy: T) -> Self { - Self::new(proxy, B256::ZERO) + Self::new(proxy, B256::ZERO, u64::MAX - 1) } pub fn with_root>(self, object_root: R) -> Self { @@ -130,15 +137,20 @@ impl SignProxyRequest { pub fn with_msg(self, msg: &impl TreeHash) -> Self { self.with_root(msg.tree_hash_root().0) } + + pub fn with_nonce(self, nonce: u64) -> Self { + Self { nonce, ..self } + } } impl Display for SignProxyRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "BLS(proxy: {}, object_root: {})", + "BLS(proxy: {}, object_root: {}, nonce: {})", self.proxy, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } @@ -147,9 +159,10 @@ impl Display for SignProxyRequest
{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "ECDSA(proxy: {}, object_root: {})", + "ECDSA(proxy: {}, object_root: {}, nonce: {})", self.proxy, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } diff --git a/crates/common/src/commit/response.rs b/crates/common/src/commit/response.rs index 543fb6fc..a5eb9434 100644 --- a/crates/common/src/commit/response.rs +++ b/crates/common/src/commit/response.rs @@ -1,5 +1,5 @@ use alloy::{ - primitives::{Address, B256}, + primitives::{Address, B256, U256}, rpc::types::beacon::BlsSignature, }; use serde::{Deserialize, Serialize}; @@ -11,6 +11,8 @@ pub struct BlsSignResponse { pub pubkey: BlsPublicKey, pub object_root: B256, pub module_signing_id: B256, + pub nonce: u64, + pub chain_id: U256, pub signature: BlsSignature, } @@ -19,9 +21,11 @@ impl BlsSignResponse { pubkey: BlsPublicKey, object_root: B256, module_signing_id: B256, + nonce: u64, + chain_id: U256, signature: BlsSignature, ) -> Self { - Self { pubkey, object_root, module_signing_id, signature } + Self { pubkey, object_root, module_signing_id, nonce, chain_id, signature } } } @@ -30,6 +34,8 @@ pub struct EcdsaSignResponse { pub address: Address, pub object_root: B256, pub module_signing_id: B256, + pub nonce: u64, + pub chain_id: U256, pub signature: EcdsaSignature, } @@ -38,8 +44,10 @@ impl EcdsaSignResponse { address: Address, object_root: B256, module_signing_id: B256, + nonce: u64, + chain_id: U256, signature: EcdsaSignature, ) -> Self { - Self { address, object_root, module_signing_id, signature } + Self { address, object_root, module_signing_id, nonce, chain_id, signature } } } diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index d04b3394..e12493d4 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -169,12 +169,13 @@ impl PbsConfig { if !matches!(chain, Chain::Custom { .. }) { let provider = ProviderBuilder::new().on_http(rpc_url.clone()); let chain_id = provider.get_chain_id().await?; + let chain_id_big = U256::from(chain_id); ensure!( - chain_id == chain.id(), + chain_id_big == chain.id(), "Rpc url is for the wrong chain, expected: {} ({:?}) got {}", chain.id(), chain, - chain_id + chain_id_big ); } } diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index c5e40a21..ebb91e11 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -177,7 +177,7 @@ mod tests { &parsed.message, &parsed.signature, None, - &B32::from(APPLICATION_BUILDER_DOMAIN) + &B32::from(APPLICATION_BUILDER_DOMAIN), ) .is_ok()) } diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index cd960031..8d034077 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -9,7 +9,7 @@ use crate::{ constants::{COMMIT_BOOST_DOMAIN, GENESIS_VALIDATORS_ROOT}, error::BlstErrorWrapper, signer::{verify_bls_signature, verify_ecdsa_signature, BlsSecretKey, EcdsaSignature}, - types::{self, Chain}, + types::{self, Chain, SignatureRequestInfo}, }; pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { @@ -20,15 +20,19 @@ pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { pub fn compute_prop_commit_signing_root( chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, domain_mask: &B32, ) -> B256 { let domain = compute_domain(chain, domain_mask); - match module_signing_id { - Some(id) => { - let object_root = - types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } - .tree_hash_root(); + match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + let object_root = types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: chain.id(), + } + .tree_hash_root(); types::SigningData { object_root, signing_domain: domain }.tree_hash_root() } None => types::SigningData { object_root: *object_root, signing_domain: domain } @@ -63,13 +67,13 @@ pub fn verify_signed_message( pubkey: &BlsPublicKey, msg: &T, signature: &BlsSignature, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, domain_mask: &B32, ) -> Result<(), BlstErrorWrapper> { let signing_root = compute_prop_commit_signing_root( chain, &msg.tree_hash_root(), - module_signing_id, + signature_request_info, domain_mask, ); verify_bls_signature(pubkey, signing_root.as_slice(), signature) @@ -100,12 +104,12 @@ pub fn sign_commit_boost_root( chain: Chain, secret_key: &BlsSecretKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { let signing_root = compute_prop_commit_signing_root( chain, object_root, - module_signing_id, + signature_request_info, &B32::from(COMMIT_BOOST_DOMAIN), ); sign_message(secret_key, signing_root.as_slice()) @@ -123,11 +127,14 @@ pub fn verify_proposer_commitment_signature_bls( msg: &impl TreeHash, signature: &BlsSignature, module_signing_id: &B256, + nonce: u64, ) -> Result<(), BlstErrorWrapper> { let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); let object_root = types::PropCommitSigningInfo { data: msg.tree_hash_root(), module_signing_id: *module_signing_id, + nonce, + chain_id: chain.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -142,12 +149,16 @@ pub fn verify_proposer_commitment_signature_ecdsa( msg: &impl TreeHash, signature: &EcdsaSignature, module_signing_id: &B256, + nonce: u64, ) -> Result<(), eyre::Report> { - let object_root = msg.tree_hash_root(); let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let object_root = - types::PropCommitSigningInfo { data: object_root, module_signing_id: *module_signing_id } - .tree_hash_root(); + let object_root = types::PropCommitSigningInfo { + data: msg.tree_hash_root(), + module_signing_id: *module_signing_id, + nonce, + chain_id: chain.id(), + } + .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); verify_ecdsa_signature(address, &signing_root, signature) } diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs index 15367f36..c5b75ce6 100644 --- a/crates/common/src/signer/schemes/bls.rs +++ b/crates/common/src/signer/schemes/bls.rs @@ -4,7 +4,9 @@ use blst::BLST_ERROR; use tree_hash::TreeHash; use crate::{ - error::BlstErrorWrapper, signature::sign_commit_boost_root, types::Chain, + error::BlstErrorWrapper, + signature::sign_commit_boost_root, + types::{Chain, SignatureRequestInfo}, utils::blst_pubkey_to_alloy, }; @@ -42,11 +44,11 @@ impl BlsSigner { &self, chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { match self { BlsSigner::Local(sk) => { - sign_commit_boost_root(chain, sk, object_root, module_signing_id) + sign_commit_boost_root(chain, sk, object_root, signature_request_info) } } } @@ -55,9 +57,9 @@ impl BlsSigner { &self, chain: Chain, msg: &impl TreeHash, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { - self.sign(chain, &msg.tree_hash_root(), module_signing_id).await + self.sign(chain, &msg.tree_hash_root(), signature_request_info).await } } diff --git a/crates/common/src/signer/schemes/ecdsa.rs b/crates/common/src/signer/schemes/ecdsa.rs index 907340f1..c01cf85f 100644 --- a/crates/common/src/signer/schemes/ecdsa.rs +++ b/crates/common/src/signer/schemes/ecdsa.rs @@ -10,7 +10,7 @@ use tree_hash::TreeHash; use crate::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - types::{self, Chain}, + types::{self, Chain, SignatureRequestInfo}, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -87,16 +87,18 @@ impl EcdsaSigner { &self, chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self { EcdsaSigner::Local(sk) => { let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let signing_root = match module_signing_id { - Some(id) => { + let signing_root = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { let object_root = types::PropCommitSigningInfo { data: *object_root, - module_signing_id: *id, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: chain.id(), } .tree_hash_root(); types::SigningData { object_root, signing_domain }.tree_hash_root() @@ -112,9 +114,9 @@ impl EcdsaSigner { &self, chain: Chain, msg: &impl TreeHash, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { - self.sign(chain, &msg.tree_hash_root(), module_signing_id).await + self.sign(chain, &msg.tree_hash_root(), signature_request_info).await } } @@ -131,7 +133,10 @@ pub fn verify_ecdsa_signature( #[cfg(test)] mod test { - use alloy::{hex, primitives::bytes}; + use alloy::{ + hex, + primitives::{b256, bytes}, + }; use super::*; @@ -161,15 +166,30 @@ mod test { let object_root = B256::from([1; 32]); let module_signing_id = B256::from([2; 32]); - let signature = - signer.sign(Chain::Hoodi, &object_root, Some(&module_signing_id)).await.unwrap(); + let nonce = 42; + let signature = signer + .sign( + Chain::Hoodi, + &object_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) + .await + .unwrap(); let signing_domain = compute_domain(Chain::Hoodi, &B32::from(COMMIT_BOOST_DOMAIN)); - let object_root = - types::PropCommitSigningInfo { data: object_root, module_signing_id }.tree_hash_root(); + let object_root = types::PropCommitSigningInfo { + data: object_root, + module_signing_id, + nonce, + chain_id: Chain::Hoodi.id(), + } + .tree_hash_root(); let msg = types::SigningData { object_root, signing_domain }.tree_hash_root(); - assert_eq!(msg, hex!("8cd49ccf2f9b0297796ff96ce5f7c5d26e20a59d0032ee2ad6249dcd9682b808")); + assert_eq!( + msg, + b256!("0x0b95fcdb3f003fc6f0fd3238d906f359809e97fe7ec71f56771cb05bee4150bd") + ); let address = signer.address(); let verified = verify_ecdsa_signature(&address, &msg, &signature); diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index c747815b..d650f6be 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use alloy::primitives::{aliases::B32, hex, Bytes, B256}; +use alloy::primitives::{aliases::B32, hex, Bytes, B256, U256}; use derive_more::{Deref, Display, From, Into}; use eyre::{bail, Context}; use serde::{Deserialize, Serialize}; @@ -72,7 +72,9 @@ impl std::fmt::Debug for Chain { } impl Chain { - pub fn id(&self) -> u64 { + // Chain IDs are 256-bit unsigned integers because they need to support + // Keccak256 hashes + pub fn id(&self) -> U256 { match self { Chain::Mainnet => KnownChain::Mainnet.id(), Chain::Holesky => KnownChain::Holesky.id(), @@ -146,13 +148,13 @@ pub enum KnownChain { // Constants impl KnownChain { - pub fn id(&self) -> u64 { + pub fn id(&self) -> U256 { match self { - KnownChain::Mainnet => 1, - KnownChain::Holesky => 17000, - KnownChain::Sepolia => 11155111, - KnownChain::Helder => 167000, - KnownChain::Hoodi => 560048, + KnownChain::Mainnet => U256::from(1), + KnownChain::Holesky => U256::from(17000), + KnownChain::Sepolia => U256::from(11155111), + KnownChain::Helder => U256::from(167000), + KnownChain::Hoodi => U256::from(560048), } } @@ -304,6 +306,15 @@ pub struct SigningData { pub struct PropCommitSigningInfo { pub data: B256, pub module_signing_id: B256, + pub nonce: u64, // As per https://eips.ethereum.org/EIPS/eip-2681 + pub chain_id: U256, +} + +/// Information about a signature request, including the module signing ID and +/// nonce. +pub struct SignatureRequestInfo { + pub module_signing_id: B256, + pub nonce: u64, } /// Returns seconds_per_slot and genesis_fork_version from a spec, such as diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index add9e3a2..3067c8a1 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -12,7 +12,7 @@ use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, signer::{BlsPublicKey, BlsSignature, ProxyStore}, - types::{self, Chain, ModuleId}, + types::{self, Chain, ModuleId, SignatureRequestInfo}, }; use eyre::{bail, OptionExt}; use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; @@ -148,6 +148,11 @@ impl DirkManager { }) } + /// Get the chain config for the manager + pub fn get_chain(&self) -> Chain { + self.chain + } + /// Set the proxy store to use for storing proxy delegations pub fn with_proxy_store(self, store: ProxyStore) -> eyre::Result { if let ProxyStore::ERC2335 { .. } = store { @@ -197,14 +202,15 @@ impl DirkManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self.consensus_accounts.get(pubkey) { Some(Account::Simple(account)) => { - self.request_simple_signature(account, object_root, module_signing_id).await + self.request_simple_signature(account, object_root, signature_request_info).await } Some(Account::Distributed(account)) => { - self.request_distributed_signature(account, object_root, module_signing_id).await + self.request_distributed_signature(account, object_root, signature_request_info) + .await } None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec())), } @@ -215,14 +221,15 @@ impl DirkManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self.proxy_accounts.get(pubkey) { Some(ProxyAccount { inner: Account::Simple(account), .. }) => { - self.request_simple_signature(account, object_root, module_signing_id).await + self.request_simple_signature(account, object_root, signature_request_info).await } Some(ProxyAccount { inner: Account::Distributed(account), .. }) => { - self.request_distributed_signature(account, object_root, module_signing_id).await + self.request_distributed_signature(account, object_root, signature_request_info) + .await } None => Err(SignerModuleError::UnknownProxySigner(pubkey.to_vec())), } @@ -233,14 +240,21 @@ impl DirkManager { &self, account: &SimpleAccount, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let domain = compute_domain(self.chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let data = match module_signing_id { - Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + let data = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: self.chain.id(), + } .tree_hash_root() - .to_vec(), + .to_vec() + } None => object_root.to_vec(), }; @@ -271,15 +285,22 @@ impl DirkManager { &self, account: &DistributedAccount, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let mut partials = Vec::with_capacity(account.participants.len()); let mut requests = Vec::with_capacity(account.participants.len()); - let data = match module_signing_id { - Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + let data = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: self.chain.id(), + } .tree_hash_root() - .to_vec(), + .to_vec() + } None => object_root.to_vec(), }; diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index a13695e5..632a01b6 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -13,7 +13,7 @@ use cb_common::{ BlsProxySigner, BlsPublicKey, BlsSigner, ConsensusSigner, EcdsaProxySigner, EcdsaSignature, EcdsaSigner, ProxySigners, ProxyStore, }, - types::{Chain, ModuleId}, + types::{Chain, ModuleId, SignatureRequestInfo}, }; use tree_hash::TreeHash; @@ -53,6 +53,11 @@ impl LocalSigningManager { Ok(manager) } + /// Get the chain config for the manager + pub fn get_chain(&self) -> Chain { + self.chain + } + pub fn add_consensus_signer(&mut self, signer: ConsensusSigner) { self.consensus_signers.insert(signer.pubkey(), signer); } @@ -133,13 +138,13 @@ impl LocalSigningManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let signer = self .consensus_signers .get(pubkey) .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; - let signature = signer.sign(self.chain, object_root, module_signing_id).await; + let signature = signer.sign(self.chain, object_root, signature_request_info).await; Ok(signature) } @@ -148,14 +153,14 @@ impl LocalSigningManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let bls_proxy = self .proxy_signers .bls_signers .get(pubkey) .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; - let signature = bls_proxy.sign(self.chain, object_root, module_signing_id).await; + let signature = bls_proxy.sign(self.chain, object_root, signature_request_info).await; Ok(signature) } @@ -163,14 +168,14 @@ impl LocalSigningManager { &self, address: &Address, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let ecdsa_proxy = self .proxy_signers .ecdsa_signers .get(address) .ok_or(SignerModuleError::UnknownProxySigner(address.to_vec()))?; - let signature = ecdsa_proxy.sign(self.chain, object_root, module_signing_id).await?; + let signature = ecdsa_proxy.sign(self.chain, object_root, signature_request_info).await?; Ok(signature) } @@ -307,9 +312,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 43; let sig = signing_manager - .sign_consensus(&consensus_pk, &data_root, Some(&module_signing_id)) + .sign_consensus( + &consensus_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -318,6 +328,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -384,9 +396,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 44; let sig = signing_manager - .sign_proxy_bls(&proxy_pk, &data_root, Some(&module_signing_id)) + .sign_proxy_bls( + &proxy_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -395,6 +412,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -463,9 +482,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 45; let sig = signing_manager - .sign_proxy_ecdsa(&proxy_pk, &data_root, Some(&module_signing_id)) + .sign_proxy_ecdsa( + &proxy_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -474,6 +498,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index f69eb650..bb168cf4 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -6,7 +6,7 @@ use std::{ }; use alloy::{ - primitives::{Address, B256}, + primitives::{Address, B256, U256}, rpc::types::beacon::BlsPublicKey, }; use axum::{ @@ -33,7 +33,7 @@ use cb_common::{ }, config::{ModuleSigningConfig, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, - types::{Chain, Jwt, ModuleId}, + types::{Chain, Jwt, ModuleId, SignatureRequestInfo}, utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; @@ -315,6 +315,7 @@ async fn handle_request_signature_bls( false, &request.pubkey, &request.object_root, + request.nonce, ) .await } @@ -335,6 +336,7 @@ async fn handle_request_signature_proxy_bls( true, &request.proxy, &request.object_root, + request.nonce, ) .await } @@ -347,6 +349,7 @@ async fn handle_request_signature_bls_impl( is_proxy: bool, signing_pubkey: &BlsPublicKey, object_root: &B256, + nonce: u64, ) -> Result { let Some(signing_id) = state.jwts.read().get(module_id).map(|m| m.signing_id) else { error!( @@ -358,28 +361,52 @@ async fn handle_request_signature_bls_impl( return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; + let chain_id: U256; match &*state.manager.read().await { SigningManager::Local(local_manager) => { + chain_id = local_manager.get_chain().id(); if is_proxy { - local_manager.sign_proxy_bls(signing_pubkey, object_root, Some(&signing_id)).await + local_manager + .sign_proxy_bls( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) + .await } else { - local_manager.sign_consensus(signing_pubkey, object_root, Some(&signing_id)).await + local_manager + .sign_consensus( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) + .await } } SigningManager::Dirk(dirk_manager) => { + chain_id = dirk_manager.get_chain().id(); if is_proxy { dirk_manager - .request_proxy_signature(signing_pubkey, object_root, Some(&signing_id)) + .request_proxy_signature( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) .await } else { dirk_manager - .request_consensus_signature(signing_pubkey, object_root, Some(&signing_id)) + .request_consensus_signature( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) .await } } } .map(|sig| { - Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, sig)).into_response() + Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, nonce, chain_id, sig)) + .into_response() }) .map_err(|err| { error!(event = "request_signature", ?module_id, ?req_id, "{err}"); @@ -406,13 +433,23 @@ async fn handle_request_signature_proxy_ecdsa( }; debug!(event = "proxy_ecdsa_request_signature", ?module_id, %request, ?req_id, "New request"); + let chain_id: U256; match &*state.manager.read().await { SigningManager::Local(local_manager) => { + chain_id = local_manager.get_chain().id(); local_manager - .sign_proxy_ecdsa(&request.proxy, &request.object_root, Some(&signing_id)) + .sign_proxy_ecdsa( + &request.proxy, + &request.object_root, + Some(&SignatureRequestInfo { + module_signing_id: signing_id, + nonce: request.nonce, + }), + ) .await } SigningManager::Dirk(_) => { + chain_id = U256::ZERO; // Dirk does not support ECDSA proxy signing error!( event = "request_signature", ?module_id, @@ -423,8 +460,15 @@ async fn handle_request_signature_proxy_ecdsa( } } .map(|sig| { - Json(EcdsaSignResponse::new(request.proxy, request.object_root, signing_id, sig)) - .into_response() + Json(EcdsaSignResponse::new( + request.proxy, + request.object_root, + signing_id, + request.nonce, + chain_id, + sig, + )) + .into_response() }) .map_err(|err| { error!(event = "request_signature", ?module_id, ?req_id, "{err}"); diff --git a/docs/docs/developing/prop-commit-signing.md b/docs/docs/developing/prop-commit-signing.md index fd19fafc..1e8bd249 100644 --- a/docs/docs/developing/prop-commit-signing.md +++ b/docs/docs/developing/prop-commit-signing.md @@ -17,6 +17,7 @@ Proposer commitment signatures produced by Commit-Boost's signer service conform - The data payload being signed must be a **32-byte array**, typically serializd as a 64-character hex string with an optional `0x` prefix. The value itself is arbitrary, as long as it has meaning to the requester - though it is typically the 256-bit hash of some kind of data. - If requesting a signature from a BLS key, the resulting signature will be a standard BLS signature (96 bytes in length). - If requesting a signature from an ECDSA key, the resulting signature will be a standard Ethereum RSV signature (65 bytes in length). +- Signatures **may** be **unique** per request, using the optional `nonce` field in their requests to indicate a unique sequence that this signature belongs to. ## Configuring a Module for Proposer Commitments @@ -37,9 +38,18 @@ Your module's signing ID is a 32-byte value that is used as a unique identifier The Signing ID is decoupled from your module's human-readable name (the `module_id` field in the Commit-Boost configuration file) so that any changes to your module name will not invalidate signatures from previous versions. Similarly, if you don't change the module ID but *want* to invalidate previous signatures, you can modify the signing ID and it will do so. Just ensure your users are made aware of the change, so they can update it in their Commit-Boost configuration files accordingly. +## Nonces + +Your module has the option of using **Nonces** for each of its signature requests. Nonces are intended to be unique values that establish a sequence of signature requests, distinguishing one signature from another - even if all of their other payload information is identical. When making a request for a signature, you may include a unique nonce as part of the request; the signature will include it in its data, ensuring that things like replay attacks cannot be used for that signature. + +If you want to use them within your module, your module (or whatever remote backend system it connects to) **will be responsible** for storing, comparing, validating, and otherwise using the nonces. Commit-Boost's signer service by itself **does not** store nonces or track which ones have already been used by a given module. + +In terms of implementation, the nonce format conforms to the specification in [EIP-2681](https://eips.ethereum.org/EIPS/eip-2681). It is an unsigned 64-bit big-endian integer, with a minimum value of 0 and a maximum value of `2^64-2`. We recommend using `2^64-1` as a signifier indicating that your module doesn't use nonces, rather than using 0 for such a purpose. + + ## Structure of a Signature -The form proposer commitment signatures take depends on the type of signature being requested. BLS signatures take the [standard form](https://eth2book.info/latest/part2/building_blocks/signatures/) (96-byte values). ECDSA (Ethereum EL) signatures take the [standard Ethereum ECDSA `r,s,v` signature form](https://forum.openzeppelin.com/t/sign-it-like-you-mean-it-creating-and-verifying-ethereum-signatures/697). In both cases, the data being signed is a 32-byte hash - the root hash of an SSZ Merkle tree, described below: +The form proposer commitment signatures take depends on the type of signature being requested. BLS signatures take the [standard form](https://eth2book.info/latest/part2/building_blocks/signatures/) (96-byte values). ECDSA (Ethereum EL) signatures take the [standard Ethereum ECDSA `r,s,v` signature form](https://forum.openzeppelin.com/t/sign-it-like-you-mean-it-creating-and-verifying-ethereum-signatures/697). In both cases, the data being signed is a 32-byte hash - the root hash of a composite two-stage [SSZ Merkle tree](https://thogiti.github.io/2024/05/02/Merkleization.html), described below:
@@ -47,14 +57,20 @@ The form proposer commitment signatures take depends on the type of signature be
-where: +where, for the sub-tree in blue: - `Request Data` is a 32-byte array that serves as the data you want to sign. This is typically a hash of some more complex data on its own that your module constructs. - `Signing ID` is your module's 32-byte signing ID. The signer service will load this for your module from its configuration file. -- `Domain` is the 32-byte output of the [compute_domain()](https://eth2book.info/capella/part2/building_blocks/signatures/#domain-separation-and-forks) function in the Beacon specification. The 4-byte domain type in this case is not a standard Beacon domain type, but rather Commit-Boost's own domain type: `0x6D6D6F43`. +- `Nonce` is the nonce value for the signature request. While this value must be present, it can be effectively ignored by setting it to some arbitrary value if your module does not track nonces. Conforming with the tree specification, it must be added as a 256-bit unsigned little-endian integer. Most libraries will be able to do this conversion automatically if you specify the field as the language's primitive for 64-bit unsigned integers (e.g., `uint64`, `u64`, `ulong`, etc.). + +- `Chain ID` is the ID of the chain that the Signer service is currently configured to use, as indicated by the [Commit-Boost configuration file](../get_started/configuration.md). This must also be a 256-bit unsigned little-endian integer. + +A Merkle tree must be constructed from these four leaf nodes, and its root hash calculated according to the standard SSZ hash computation rules. This result will be called the "sub-tree root". With this, a second Merkle tree is created using this sub-tree root and a value called the Domain: + +- `Domain` is the 32-byte output of the [compute_domain()](https://eth2book.info/capella/part2/building_blocks/signatures/#domain-separation-and-forks) function in the Beacon specification. The 4-byte domain type in this case is not a standard Beacon domain type, but rather Commit Boost's own domain type: `0x6D6D6F43`. -The data signed in a proposer commitment is the 32-byte root of this tree (the green `Root` box). Note that calculating this will involve calculating the Merkle Root of two separate trees: first the blue data subtree (with the original request data and the signing ID) to establish the blue `Root` value, and then again with a tree created from that value and the `Domain`. +The data signed in a proposer commitment is the 32-byte hash root of this new tree (the green `Root` box). Many languages provide libraries for computing the root of an SSZ Merkle tree, such as [fastssz for Go](https://github.com/ferranbt/fastssz) or [tree_hash for Rust](https://docs.rs/tree_hash/latest/tree_hash/). When verifying proposer commitment signatures, use a library that supports Merkle tree root hashing, the `compute_domain()` operation, and validation for signatures generated by your key of choice. diff --git a/docs/docs/res/img/prop_commit_tree.png b/docs/docs/res/img/prop_commit_tree.png index 1e36f4b4..2c0b1815 100644 Binary files a/docs/docs/res/img/prop_commit_tree.png and b/docs/docs/res/img/prop_commit_tree.png differ diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 8360d07d..3edd6bc2 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -32,6 +32,7 @@ struct Datagram { struct DaCommitService { config: StartCommitModuleConfig, + nonce: u64, } // Extra configurations parameters can be set here and will be automatically @@ -100,10 +101,12 @@ impl DaCommitService { &datagram, &response.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; + self.nonce += 1; // Request a signature from a proxy BLS key let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); @@ -116,10 +119,12 @@ impl DaCommitService { &datagram, &proxy_response_bls.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; + self.nonce += 1; // If ECDSA keys are enabled, request a signature from a proxy ECDSA key if let Some(proxy_ecdsa) = proxy_ecdsa { @@ -136,11 +141,13 @@ impl DaCommitService { &datagram, &proxy_response_ecdsa.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; } + self.nonce += 1; SIG_RECEIVED_COUNTER.inc(); @@ -168,7 +175,7 @@ async fn main() -> Result<()> { "Starting module with custom data" ); - let mut service = DaCommitService { config }; + let mut service = DaCommitService { config, nonce: 0 }; if let Err(err) = service.run().await { error!(%err, "Service failed"); diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index f5c4e6c1..deb3bd39 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -12,7 +12,7 @@ use cb_common::{ }, config::{load_module_signing_configs, ModuleSigningConfig}, signer::BlsSignature, - types::ModuleId, + types::{Chain, ModuleId}, utils::create_jwt, }; use cb_tests::{ @@ -62,7 +62,8 @@ async fn test_signer_sign_request_good() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; + let nonce: u64 = 101; + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -77,7 +78,9 @@ async fn test_signer_sign_request_good() -> Result<()> { BlsPublicKey::from(PUBKEY_1), object_root, mod_cfgs.get(&module_id).unwrap().signing_id, - BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap()); + nonce, + Chain::Hoodi.id(), + BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap()); assert_eq!(sig_response, expected, "Signature response does not match expected value"); Ok(()) @@ -95,7 +98,8 @@ async fn test_signer_sign_request_different_module() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; + let nonce: u64 = 101; + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -118,7 +122,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { "Module signing ID does not match expected value" ); assert_ne!( - sig_response.signature, BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap(), + sig_response.signature, BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap(), "Signature matches the reference signature, which should not happen" );