Skip to content

Commit

Permalink
Merge branch 'unstable' into lore/feat/holesky-launch
Browse files Browse the repository at this point in the history
  • Loading branch information
thedevbirb committed Oct 30, 2024
2 parents f0d2f88 + b91f90b commit a24a599
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 14 deletions.
6 changes: 5 additions & 1 deletion .github/.linkspector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ excludedDirs:
- bolt-contracts/lib
- testnets/helder

useGitIgnore: true
useGitIgnore: true

ignorePatterns:
- pattern: "^https://.*etherscan.io/.*$"

192 changes: 192 additions & 0 deletions bolt-sidecar/src/chain_io/manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::str::FromStr;

use alloy::{
contract::Error as ContractError,
primitives::{Address, Bytes},
providers::{ProviderBuilder, RootProvider},
sol,
sol_types::SolInterface,
transports::{http::Http, TransportError},
};
use ethereum_consensus::primitives::BlsPublicKey;
use eyre::bail;
use reqwest::{Client, Url};
use serde::Serialize;

use BoltManagerContract::{BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus};

use crate::config::chain::Chain;

use super::utils::{self, CompressedHash};

/// A wrapper over a BoltManagerContract that exposes various utility methods.
#[derive(Debug, Clone)]
pub struct BoltManager(BoltManagerContractInstance<Http<Client>, RootProvider<Http<Client>>>);

impl BoltManager {
/// Creates a new BoltRegistry instance. Returns `None` if a canonical BoltManager contract is
/// not deployed on such chain.
///
/// TODO: change after https://github.com/chainbound/bolt/issues/343 is completed
pub fn from_chain<U: Into<Url>>(execution_client_url: U, chain: Chain) -> Option<Self> {
let address = chain.manager_address()?;
Some(Self::from_address(execution_client_url, address))
}

/// Creates a new BoltRegistry instance.
pub fn from_address<U: Into<Url>>(execution_client_url: U, manager_address: Address) -> Self {
let provider = ProviderBuilder::new().on_http(execution_client_url.into());
let registry = BoltManagerContract::new(manager_address, provider);

Self(registry)
}

/// Verify the provided validator public keys are registered in Bolt and are active
/// and their authorized operator is the given commitment signer public key.
///
/// NOTE: it also checks the operator associated to the `commitment_signer_pubkey` exists.
pub async fn verify_validator_pubkeys(
&self,
keys: &[BlsPublicKey],
commitment_signer_pubkey: Address,
) -> eyre::Result<Vec<ProposerStatus>> {
let hashes = utils::pubkey_hashes(keys);

let returndata = self.0.getProposerStatuses(hashes).call().await;

// TODO: clean this after https://github.com/alloy-rs/alloy/issues/787 is merged
let error = match returndata.map(|data| data.statuses) {
Ok(statuses) => {
for status in &statuses {
if !status.active {
bail!(
"validator with public key hash {:?} is not active in Bolt",
status.pubkeyHash
);
} else if status.operator != commitment_signer_pubkey {
bail!(generate_operator_keys_mismatch_error(
status.pubkeyHash,
commitment_signer_pubkey,
status.operator
));
}
}

return Ok(statuses);
}
Err(error) => match error {
ContractError::TransportError(TransportError::ErrorResp(err)) => {
let data = err.data.unwrap_or_default();
let data = data.get().trim_matches('"');
let data = Bytes::from_str(data)?;

BoltManagerContractErrors::abi_decode(&data, true)?
}
e => return Err(e)?,
},
};

match error {
BoltManagerContractErrors::ValidatorDoesNotExist(pubkey_hash) => {
bail!("validator with public key hash {:?} is not registered in Bolt", pubkey_hash);
}
BoltManagerContractErrors::InvalidQuery(_) => {
bail!("invalid zero public key hash");
}
BoltManagerContractErrors::KeyNotFound(_) => {
bail!("operator associated with commitment signer public key {:?} is not registered in Bolt", commitment_signer_pubkey);
}
}
}
}

fn generate_operator_keys_mismatch_error(
pubkey_hash: CompressedHash,
commitment_signer_pubkey: Address,
operator: Address,
) -> String {
format!(
"mismatch between commitment signer public key and authorized operator address for validator with public key hash {:?} in Bolt.\n - commitment signer public key: {:?}\n - authorized operator address: {:?}",
pubkey_hash,
commitment_signer_pubkey,
operator
)
}

sol! {
#[allow(missing_docs)]
#[sol(rpc)]
interface BoltManagerContract {
#[derive(Debug, Default, Serialize)]
struct ProposerStatus {
bytes20 pubkeyHash;
bool active;
address operator;
string operatorRPC;
address[] collaterals;
uint256[] amounts;
}

function getProposerStatuses(bytes20[] calldata pubkeyHashes) public view returns (ProposerStatus[] memory statuses);

function isOperator(address operator) external view returns (bool isOperator);

error KeyNotFound();
error InvalidQuery();
#[derive(Debug)]
error ValidatorDoesNotExist(bytes20 pubkeyHash);
}
}

#[cfg(test)]
mod tests {
use ::hex::FromHex;
use alloy::{hex, primitives::Address};
use ethereum_consensus::primitives::BlsPublicKey;
use reqwest::Url;

use crate::{
chain_io::{manager::generate_operator_keys_mismatch_error, utils::pubkey_hash},
config::chain::Chain,
};

use super::BoltManager;

#[tokio::test]
#[ignore = "requires Chainbound tailnet"]
async fn test_verify_validator_pubkeys() {
let url = Url::parse("http://remotebeast:48545").expect("valid url");
let manager =
BoltManager::from_chain(url, Chain::Holesky).expect("manager deployed on Holesky");

let operator =
Address::from_hex("725028b0b7c3db8b8242d35cd3a5779838b217b1").expect("valid address");

let keys = vec![BlsPublicKey::try_from([0; 48].as_ref()).expect("valid bls public key")];
let commitment_signer_pubkey = Address::ZERO;

let res = manager.verify_validator_pubkeys(&keys, commitment_signer_pubkey).await;
assert!(res.unwrap_err().to_string().contains("ValidatorDoesNotExist"));

let keys = vec![
BlsPublicKey::try_from(
hex!("87cbbfe6f08a0fd424507726cfcf5b9df2b2fd6b78a65a3d7bb6db946dca3102eb8abae32847d5a9a27e414888414c26")
.as_ref()).expect("valid bls public key")];
let res = manager.verify_validator_pubkeys(&keys, commitment_signer_pubkey).await;
assert!(
res.unwrap_err().to_string()
== generate_operator_keys_mismatch_error(
pubkey_hash(&keys[0]),
commitment_signer_pubkey,
operator
)
);

let commitment_signer_pubkey = operator;
let res = manager
.verify_validator_pubkeys(&keys, commitment_signer_pubkey)
.await
.expect("active validator and correct operator");
assert!(res[0].active);
}
}
5 changes: 5 additions & 0 deletions bolt-sidecar/src/chain_io/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// Wrapper over the BoltManager contract
pub mod manager;

/// Utilities and functions used in the Bolt contracts
pub mod utils;
52 changes: 52 additions & 0 deletions bolt-sidecar/src/chain_io/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use alloy::primitives::{FixedBytes, B512};
use ethereum_consensus::primitives::BlsPublicKey;
use reth_primitives::keccak256;

/// A 20-byte compressed hash of a BLS public key.
///
/// Reference: https://github.com/chainbound/bolt/blob/bec46baae6d7c16dddd81e5e72710ca8e3064f82/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol#L65-L69
pub(crate) type CompressedHash = FixedBytes<20>;

/// Hash the public keys of the proposers. This follows the same
/// implementation done on-chain in the BoltValidators contract.
pub fn pubkey_hashes(keys: &[BlsPublicKey]) -> Vec<CompressedHash> {
keys.iter().map(pubkey_hash).collect()
}

/// Hash the public key of the proposer. This follows the same
/// implementation done on-chain in the BoltValidators contract.
///
/// Reference: https://github.com/chainbound/bolt/blob/bec46baae6d7c16dddd81e5e72710ca8e3064f82/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol#L65-L69
pub fn pubkey_hash(key: &BlsPublicKey) -> CompressedHash {
let digest = pubkey_hash_digest(key);
let hash = keccak256(digest);
CompressedHash::from_slice(hash.get(0..20).expect("hash is longer than 20 bytes"))
}

fn pubkey_hash_digest(key: &BlsPublicKey) -> B512 {
let mut onchain_pubkey_repr = B512::ZERO;

// copy the pubkey bytes into the rightmost 48 bytes of the 512-bit buffer.
// the result should look like this:
//
// 0x00000000000000000000000000000000b427fd179b35ef085409e4a98fb3ab84ee29c689df5c64020eab0b20a4f91170f610177db172dc091682df627c9f4021
// |<---------- 16 bytes ---------->||<----------------------------------------- 48 bytes ----------------------------------------->|
onchain_pubkey_repr[16..].copy_from_slice(key);
onchain_pubkey_repr
}

#[cfg(test)]
mod tests {
use alloy::hex;
use ethereum_consensus::primitives::BlsPublicKey;

use super::pubkey_hash;

#[test]
fn test_public_key_hash() {
let bytes = hex!("87cbbfe6f08a0fd424507726cfcf5b9df2b2fd6b78a65a3d7bb6db946dca3102eb8abae32847d5a9a27e414888414c26").as_ref();
let bls_public_key = BlsPublicKey::try_from(bytes).expect("valid bls public key");
let hash = pubkey_hash(&bls_public_key);
assert_eq!(hex::encode(hash.as_slice()), "cf44d8bca49d695164be6796108cf788d8d056e1");
}
}
14 changes: 14 additions & 0 deletions bolt-sidecar/src/config/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{
time::Duration,
};

use alloy::primitives::{address, Address};
use clap::{Args, ValueEnum};
use ethereum_consensus::deneb::{compute_fork_data_root, Root};
use serde::Deserialize;
Expand Down Expand Up @@ -31,6 +32,11 @@ pub const DEFAULT_CHAIN_CONFIG: ChainConfig = ChainConfig {
enable_unsafe_lookahead: false,
};

/// The address of the canonical BoltManager contract for the Holesky chain.
///
/// https://holesky.etherscan.io/address/0x440202829b493F9FF43E730EB5e8379EEa3678CF
pub const MANAGER_ADDRESS_HOLESKY: Address = address!("440202829b493F9FF43E730EB5e8379EEa3678CF");

/// Configuration for the chain the sidecar is running on.
#[derive(Debug, Clone, Copy, Args, Deserialize)]
pub struct ChainConfig {
Expand Down Expand Up @@ -99,6 +105,14 @@ impl Chain {
Chain::Kurtosis => [16, 0, 0, 56],
}
}

/// Returns the address of the canonical BoltManager contract for a given chain, if present
pub const fn manager_address(&self) -> Option<Address> {
match self {
Chain::Holesky => Some(MANAGER_ADDRESS_HOLESKY),
_ => None,
}
}
}

impl Display for Chain {
Expand Down
11 changes: 10 additions & 1 deletion bolt-sidecar/src/crypto/ecdsa.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::fmt::Debug;

use alloy::signers::{local::PrivateKeySigner, Signature as AlloySignature, Signer};
use alloy::{
primitives::Address,
signers::{local::PrivateKeySigner, Signature as AlloySignature, Signer},
};
use secp256k1::{ecdsa::Signature, Message, PublicKey, SecretKey};

/// Trait for any types that can be signed and verified with ECDSA.
Expand Down Expand Up @@ -57,12 +60,18 @@ impl ECDSASigner {
/// A generic signing trait to generate ECDSA signatures.
#[async_trait::async_trait]
pub trait SignerECDSA: Send + Debug {
/// Returns the public key of the signer.
fn public_key(&self) -> Address;
/// Sign the given hash and return the signature.
async fn sign_hash(&self, hash: &[u8; 32]) -> eyre::Result<AlloySignature>;
}

#[async_trait::async_trait]
impl SignerECDSA for PrivateKeySigner {
fn public_key(&self) -> Address {
self.address()
}

async fn sign_hash(&self, hash: &[u8; 32]) -> eyre::Result<AlloySignature> {
Ok(Signer::sign_hash(self, hash.into()).await?)
}
Expand Down
17 changes: 17 additions & 0 deletions bolt-sidecar/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use tracing::{debug, error, info, warn};

use crate::{
builder::payload_fetcher::LocalPayloadFetcher,
chain_io::manager::BoltManager,
commitments::{
server::{CommitmentsApiServer, Event as CommitmentEvent},
spec::Error as CommitmentError,
Expand Down Expand Up @@ -161,6 +162,22 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
commitment_signer: ECDSA,
fetcher: C,
) -> eyre::Result<Self> {
// Verify the operator and validator keys with the bolt manager
if let Some(bolt_manager) =
BoltManager::from_chain(opts.execution_api_url.clone(), opts.chain.chain)
{
let commitment_signer_pubkey = commitment_signer.public_key();
let available_pubkeys = Vec::from_iter(constraint_signer.available_pubkeys());
bolt_manager
.verify_validator_pubkeys(&available_pubkeys, commitment_signer_pubkey)
.await?;
info!(
pubkeys_len = available_pubkeys.len(),
commitment_signer_pubkey = ?commitment_signer_pubkey,
"Validators and operator keys verified with Bolt Manager successfully"
);
}

let beacon_client = BeaconClient::new(opts.beacon_api_url.clone());
let execution = ExecutionState::new(fetcher, opts.limits).await?;

Expand Down
3 changes: 3 additions & 0 deletions bolt-sidecar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub mod state;
mod signer;
pub use signer::{commit_boost::CommitBoostSigner, SignerBLS};

/// Utilities and contracts wrappers for interacting with the Bolt registry
pub mod chain_io;

/// Utilities for testing
#[cfg(test)]
mod test_util;
6 changes: 5 additions & 1 deletion bolt-sidecar/src/signer/commit_boost.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{str::FromStr, sync::Arc};

use alloy::{rpc::types::beacon::BlsSignature, signers::Signature};
use alloy::{primitives::Address, rpc::types::beacon::BlsSignature, signers::Signature};
use cb_common::{
commit::{client::SignerClient, error::SignerClientError, request::SignConsensusRequest},
signer::EcdsaPublicKey,
Expand Down Expand Up @@ -142,6 +142,10 @@ impl CommitBoostSigner {

#[async_trait::async_trait]
impl SignerECDSA for CommitBoostSigner {
fn public_key(&self) -> Address {
Address::try_from(self.get_proxy_ecdsa_pubkey().as_ref()).expect("valid address")
}

async fn sign_hash(&self, hash: &[u8; 32]) -> eyre::Result<Signature> {
let request = SignProxyRequest::builder(
*self.proxy_ecdsa.read().first().expect("proxy ecdsa key loaded"),
Expand Down
Loading

0 comments on commit a24a599

Please sign in to comment.