Skip to content

Commit

Permalink
Merge pull request #88 from chainbound/feat/signer
Browse files Browse the repository at this point in the history
Integrate generic signer in sidecar
  • Loading branch information
merklefruit authored Jun 21, 2024
2 parents f7b8b2b + 1745ea2 commit 9880b4d
Show file tree
Hide file tree
Showing 12 changed files with 838 additions and 184 deletions.
513 changes: 446 additions & 67 deletions bolt-sidecar/Cargo.lock

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions bolt-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ futures = "0.3"

# crypto
blst = "0.3.12"
secp256k1 = { version = "0.29.0", features = ["hashes", "rand"] }
secp256k1 = { version = "0.29.0", features = ["rand"] }

# alloy
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", features = [
"reqwest",
"ws",
"pubsub",
"reqwest",
"ws",
"pubsub",
] }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", features = [
"ws",
"ws",
] }
alloy-signer = { git = "https://github.com/alloy-rs/alloy" }
alloy-signer-wallet = { git = "https://github.com/alloy-rs/alloy" }
Expand All @@ -34,10 +34,10 @@ alloy-pubsub = { git = "https://github.com/alloy-rs/alloy" }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" }
alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy" }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", features = [
"k256",
"k256",
] }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy" }
alloy-primitives = "0.7.1"
alloy-primitives = { version = "0.7.1", features = ["rand"] }
alloy-rlp = "0.3"

reqwest = "0.12"
Expand All @@ -62,6 +62,10 @@ rand = "0.8.5"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"

# commit-boost
cb-crypto = { git = "https://github.com/Commit-Boost/commit-boost-client" }
cb-common = { git = "https://github.com/Commit-Boost/commit-boost-client" }

[dev-dependencies]
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy" }

Expand Down
199 changes: 199 additions & 0 deletions bolt-sidecar/src/client/commit_boost.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
use std::sync::Arc;

use alloy_rpc_types_beacon::{BlsPublicKey, BlsSignature};
use cb_common::pbs::{COMMIT_BOOST_API, PUBKEYS_PATH, SIGN_REQUEST_PATH};
use cb_crypto::types::SignRequest;
use ethereum_consensus::ssz::prelude::ssz_rs;
use parking_lot::RwLock;
use thiserror::Error;

use crate::crypto::bls::SignerBLSAsync;

const SIGN_REQUEST_ID: &str = "bolt";

#[derive(Debug, Clone)]
pub struct CommitBoostClient {
base_url: String,
client: reqwest::Client,
pubkeys: Arc<RwLock<Vec<BlsPublicKey>>>,
}

#[derive(Debug, Error)]
pub enum CommitBoostError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Deserialize(#[from] serde_json::Error),
#[error("failed to compute hash tree root for constraint {0:?}")]
HashTreeRoot(#[from] ssz_rs::MerkleizationError),
#[error("failed to sign constraint: {0}")]
NoSignature(String),
}

impl CommitBoostClient {
pub async fn new(base_url: impl Into<String>) -> Result<Self, CommitBoostError> {
let client = Self {
base_url: base_url.into(),
client: reqwest::Client::new(),
pubkeys: Arc::new(RwLock::new(Vec::new())),
};

let mut this = client.clone();
tokio::spawn(async move {
this.load_pubkeys().await.expect("failed to load pubkeys");
});

Ok(client)
}

async fn load_pubkeys(&mut self) -> Result<(), CommitBoostError> {
loop {
let url = self.url_from_path(PUBKEYS_PATH);

tracing::info!(url, "Loading public keys from commit-boost");

let response = match self.client.get(url).send().await {
Ok(res) => res,
Err(e) => {
tracing::error!(err = ?e, "failed to get public keys from commit-boost, retrying...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
}
};

let status = response.status();
let response_bytes = response.bytes().await?;

if !status.is_success() {
let err = String::from_utf8_lossy(&response_bytes).into_owned();
tracing::error!(err, ?status, "failed to get public keys, retrying...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
}

let pubkeys: Vec<BlsPublicKey> = serde_json::from_slice(&response_bytes)?;

{
let mut pk = self.pubkeys.write();
*pk = pubkeys;
return Ok(());
} // drop write lock
}
}

#[inline]
fn url_from_path(&self, path: &str) -> String {
format!("{}{COMMIT_BOOST_API}{path}", self.base_url)
}
}

#[async_trait::async_trait]
impl SignerBLSAsync for CommitBoostClient {
async fn sign(&self, data: &[u8]) -> eyre::Result<BlsSignature> {
let root = if data.len() == 32 {
let mut root = [0u8; 32];
root.copy_from_slice(data);
Ok(root)
} else {
Err(CommitBoostError::NoSignature(format!(
"invalid data length. Expected 32 bytes, found {} bytes",
data.len()
)))
}?;

let request = SignRequest::builder(
SIGN_REQUEST_ID,
*self.pubkeys.read().first().expect("pubkeys loaded"),
)
.with_root(root);

let url = self.url_from_path(SIGN_REQUEST_PATH);

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

let response = reqwest::Client::new()
.post(url)
.json(&request)
.send()
.await?;

let status = response.status();
let response_bytes = response.bytes().await?;

if !status.is_success() {
let err = String::from_utf8_lossy(&response_bytes).into_owned();
tracing::error!(err, "failed to get signature");
return Err(eyre::eyre!(CommitBoostError::NoSignature(err)));
}

let sig = serde_json::from_slice(&response_bytes)?;
Ok(sig)
}
}

#[cfg(test)]
mod tests {
use alloy_node_bindings::{Anvil, AnvilInstance};
use alloy_primitives::{hex, Address, U256};
use alloy_provider::network::{EthereumSigner, TransactionBuilder};
use alloy_rpc_types::TransactionRequest;
use alloy_signer::SignerSync;
use alloy_signer_wallet::LocalWallet;
use ssz_rs::HashTreeRoot;

use crate::primitives::{ConstraintsMessage, InclusionRequest};

use super::*;

fn launch_anvil() -> AnvilInstance {
Anvil::new().block_time(1).chain_id(1337).spawn()
}

#[tokio::test]
async fn test_commit_boost_signature() {
let _ = tracing_subscriber::fmt::try_init();

let anvil = launch_anvil();

let wallet: LocalWallet = anvil.keys()[0].clone().into();

let sender = anvil.addresses()[0];

let client = CommitBoostClient::new("http://localhost:33950")
.await
.unwrap();

let tx_request = default_transaction(sender);

let sig = wallet.sign_message_sync(&hex!("abcd")).unwrap();

let signer: EthereumSigner = wallet.into();
let signed = tx_request.build(&signer).await.unwrap();

let req = InclusionRequest {
slot: 20,
tx: signed,
signature: sig,
};

let message = ConstraintsMessage::build(0, 0, req).unwrap();
let root = message.hash_tree_root().unwrap();
let signature = client.sign(root.as_ref()).await.unwrap();

println!("Message signed, signature: {signature}")
// assert!(signature.verify(&message.hash_tree_root(), &client.pubkeys.first().unwrap()));
}

fn default_transaction(sender: Address) -> TransactionRequest {
TransactionRequest::default()
.with_from(sender)
// Burn it
.with_to(Address::random())
.with_chain_id(1337)
.with_nonce(0)
.with_value(U256::from(100))
.with_gas_limit(21_000)
.with_max_priority_fee_per_gas(1_000_000_000)
.with_max_fee_per_gas(20_000_000_000)
}
}
1 change: 1 addition & 0 deletions bolt-sidecar/src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod commit_boost;
pub mod pubsub;
pub mod rpc;
43 changes: 35 additions & 8 deletions bolt-sidecar/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use blst::min_pk::SecretKey;
use clap::Parser;
use clap::{ArgGroup, Parser};

use crate::crypto::bls::random_bls_secret;

Expand All @@ -9,9 +9,6 @@ pub struct Opts {
/// Port to listen on for incoming JSON-RPC requests
#[clap(short = 'p', long)]
pub(super) port: Option<u16>,
/// Private key to use for signing preconfirmation requests
#[clap(short = 'k', long)]
pub(super) private_key: String,
/// URL for the beacon client
#[clap(short = 'c', long)]
pub(super) beacon_client_url: String,
Expand All @@ -21,6 +18,24 @@ pub struct Opts {
/// Max commitments to accept per block
#[clap(short = 'm', long)]
pub(super) max_commitments: Option<usize>,
/// Signing options
#[clap(flatten)]
pub(super) signing: SigningOpts,
}

/// Command-line options for signing
#[derive(Debug, Clone, clap::Args)]
#[clap(
group = ArgGroup::new("signing-opts").required(true)
.args(&["private_key", "commit_boost_url"])
)]
pub struct SigningOpts {
/// Private key to use for signing preconfirmation requests
#[clap(short = 'k', long)]
pub(super) private_key: Option<String>,
/// URL for the commit-boost sidecar
#[clap(short = 'C', long, conflicts_with("private_key"))]
pub(super) commit_boost_url: Option<String>,
}

/// Configuration options for the sidecar
Expand All @@ -30,10 +45,12 @@ pub struct Config {
pub rpc_port: u16,
/// URL for the MEV-Boost sidecar client to use
pub mevboost_url: String,
/// URL for the commit-boost sidecar
pub commit_boost_url: Option<String>,
/// URL for the beacon client API URL
pub beacon_client_url: String,
/// Private key to use for signing preconfirmation requests
pub private_key: SecretKey,
pub private_key: Option<SecretKey>,
/// Limits for the sidecar
pub limits: Limits,
}
Expand All @@ -43,8 +60,9 @@ impl Default for Config {
Self {
rpc_port: 8000,
mevboost_url: "http://localhost:3030".to_string(),
commit_boost_url: None,
beacon_client_url: "http://localhost:5052".to_string(),
private_key: random_bls_secret(),
private_key: Some(random_bls_secret()),
limits: Limits::default(),
}
}
Expand All @@ -64,10 +82,19 @@ impl TryFrom<Opts> for Config {
config.limits.max_commitments_per_slot = max_commitments;
}

config.commit_boost_url = opts
.signing
.commit_boost_url
.map(|url| url.trim_end_matches('/').to_string());
config.beacon_client_url = opts.beacon_client_url.trim_end_matches('/').to_string();
config.mevboost_url = opts.mevboost_url.trim_end_matches('/').to_string();
config.private_key = SecretKey::from_bytes(&hex::decode(opts.private_key)?)
.map_err(|e| eyre::eyre!("Failed decoding BLS secret key: {:?}", e))?;
config.private_key = if let Some(sk) = opts.signing.private_key {
let sk = SecretKey::from_bytes(&hex::decode(sk)?)
.map_err(|e| eyre::eyre!("Failed decoding BLS secret key: {:?}", e))?;
Some(sk)
} else {
None
};

Ok(config)
}
Expand Down
Loading

0 comments on commit 9880b4d

Please sign in to comment.