Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sidecar): sign messages using the correct signing domain #281

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
Comment on lines -112 to +116
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_msg() was calling tree_hash_root() internally. Since we already have a 32 bytes message, we don't need to do this. If a tree hash root is required, it must be hashed by the caller.


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
Loading