Skip to content

Commit

Permalink
Merge pull request #281 from chainbound/nico/sidecar/bls-cb-sig
Browse files Browse the repository at this point in the history
feat(sidecar): sign messages using the correct signing domain
  • Loading branch information
merklefruit authored Oct 8, 2024
2 parents 806c85b + a1b1c7e commit 410c1db
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 130 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
*_dump.log
target/
.vscode
.idea
.DS_Store
.env
4 changes: 3 additions & 1 deletion bolt-boost/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
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);
}
}
126 changes: 82 additions & 44 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 @@ -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<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.
#[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<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 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<T: SignableBLS>(
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<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 +139,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_commit_boost_root(msg.digest(), &sig).is_ok());
}
}
Loading

0 comments on commit 410c1db

Please sign in to comment.