Skip to content

Commit

Permalink
feat(sidecar): sign messages using the correct signing domain
Browse files Browse the repository at this point in the history
  • Loading branch information
merklefruit committed Oct 7, 2024
1 parent 56bd71d commit 172f67c
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 116 deletions.
2 changes: 1 addition & 1 deletion bolt-sidecar/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
12 changes: 6 additions & 6 deletions bolt-sidecar/src/client/commit_boost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ impl CommitBoostSigner {

#[async_trait::async_trait]
impl SignerBLS for CommitBoostSigner {
async fn sign(&self, data: &[u8; 32]) -> eyre::Result<BlsSignature> {
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<BlsSignature> {
let request = SignConsensusRequest {
pubkey: *self.pubkeys.read().first().expect("consensus pubkey loaded"),
object_root: *data,
};

debug!(?request, "Requesting signature from commit_boost");

Expand Down Expand Up @@ -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();
Expand Down
94 changes: 68 additions & 26 deletions bolt-sidecar/src/config/chain.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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],
Expand All @@ -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)]
Expand All @@ -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);
}
}
73 changes: 42 additions & 31 deletions bolt-sidecar/src/crypto/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_";

Expand All @@ -19,14 +22,6 @@ 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.
Expand All @@ -37,31 +32,56 @@ pub trait SignableBLS {
}

/// 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<BLSSig>;
async fn sign_commit_boost_root(&self, data: &[u8; 32]) -> eyre::Result<BLSSig>;
}

/// A BLS signer that can sign any type that implements the `Signable` trait.
/// A BLS signer that can sign any type that implements the [`SignableBLS`] trait.
#[derive(Debug, Clone)]
pub struct Signer {
chain: ChainConfig,
key: BlsSecretKey,
}

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<BLSSig> {
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<BLSSig> {
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<BLSSig> {
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 of the object with the given public key.
#[allow(dead_code)]
pub fn verify<T: SignableBLS>(
&self,
obj: &T,
Expand All @@ -74,9 +94,8 @@ impl Signer {

#[async_trait::async_trait]
impl SignerBLS for Signer {
async fn sign(&self, data: &[u8; 32]) -> eyre::Result<BLSSig> {
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<BLSSig> {
self.sign_commit_boost_root(*data)
}
}

Expand All @@ -93,35 +112,27 @@ 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();
let mut data = [0u8; 32];
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(&msg, &sig, &signer.pubkey()));
}
}
25 changes: 14 additions & 11 deletions bolt-sidecar/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ impl SidecarDriver<StateClient, BlsSigner, PrivateKeySigner> {
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.
Expand Down Expand Up @@ -221,21 +222,23 @@ impl<C: StateFetcher, BLS: SignerBLS, ECDSA: SignerECDSA> SidecarDriver<C, BLS,
// parse the request into constraints and sign them
let slot = inclusion_request.slot;

// NOTE: we iterate over the transactions in the request and generate a signed constraint for each one. This is because
// the transactions in the commitment request are not supposed to be treated as a relative-ordering bundle, but a batch
// NOTE: we iterate over the transactions in the request and generate a signed constraint
// for each one. This is because the transactions in the commitment request are not
// supposed to be treated as a relative-ordering bundle, but a batch
// with no ordering guarantees.
for tx in inclusion_request.txs {
let tx_type = tx.tx_type();
let message = ConstraintsMessage::from_transaction(validator_pubkey.clone(), slot, tx);

let signed_constraints = match self.constraint_signer.sign(&message.digest()).await {
Ok(signature) => 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);
Expand Down
Loading

0 comments on commit 172f67c

Please sign in to comment.