Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
1,253 changes: 740 additions & 513 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version = "0.8.1-rc.1"

[workspace.dependencies]
aes = "0.8"
alloy = { version = "0.12", features = [
alloy = { version = "^1.0.35", features = [
"full",
"getrandom",
"providers",
Expand All @@ -18,13 +18,15 @@ alloy = { version = "0.12", features = [
"signer-local",
"ssz",
] }
alloy-primitives = "^1.3.1"
async-trait = "0.1.80"
axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10.0", features = ["typed-header"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
base64 = "0.22.1"
bimap = { version = "0.6.3", features = ["serde"] }
blsful = "2.5"
blst = "^0.3.15"
blsful = "^2.5"
bytes = "1.10.1"
cb-cli = { path = "crates/cli" }
cb-common = { path = "crates/common" }
Expand All @@ -40,16 +42,16 @@ derive_more = { version = "2.0.1", features = ["deref", "display", "from", "into
docker-compose-types = "0.16.0"
docker-image = "0.2.1"
ethereum_serde_utils = "0.7.0"
ethereum_ssz = "0.8"
ethereum_ssz_derive = "0.8"
ethereum_ssz = "0.9"
ethereum_ssz_derive = "0.9"
eyre = "0.6.12"
futures = "0.3.30"
headers = "0.4.0"
indexmap = "2.2.6"
jsonwebtoken = { version = "9.3.1", default-features = false }
lazy_static = "1.5.0"
lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v7.1.0" }
lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v7.1.0" }
lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" }
lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" }
parking_lot = "0.12.3"
pbkdf2 = "0.12.2"
prometheus = "0.14.0"
Expand All @@ -63,7 +65,8 @@ serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
serde_yaml = "0.9.33"
sha2 = "0.10.8"
ssz_types = "0.10"
ssz_types = "0.11"
subtle = "2.5"
tempfile = "3.20.0"
thiserror = "2.0.12"
tokio = { version = "1.37.0", features = ["full"] }
Expand All @@ -74,9 +77,13 @@ tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
tree_hash = "0.9"
tracing-test = { version = "0.2.5", features = ["no-env-filter"] }
tree_hash = "^0.10"
tree_hash_derive = "0.9"
typenum = "1.17.0"
unicode-normalization = "0.1.24"
url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["fast-rng", "serde", "v4"] }

[patch.crates-io]
blstrs_plus = { git = "https://github.com/Commit-Boost/blstrs" }
5 changes: 5 additions & 0 deletions api/signer-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ paths:

The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `route` (string): The route being requested (must be `/signer/v1/get_pubkeys` for this endpoint).
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
tags:
- Signer
Expand Down Expand Up @@ -73,6 +74,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/bls` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -220,6 +222,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-bls` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -367,6 +370,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-ecdsa` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -514,6 +518,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/generate_proxy_key` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down
3 changes: 2 additions & 1 deletion bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ pub mod prelude {
load_commit_module_config, load_pbs_config, load_pbs_custom_config,
},
signature::{
verify_proposer_commitment_signature_bls, verify_proposer_commitment_signature_ecdsa,
verify_proposer_commitment_signature_bls_for_message,
verify_proposer_commitment_signature_ecdsa_for_message,
},
signer::EcdsaSignature,
types::{BlsPublicKey, BlsSignature, Chain},
Expand Down
3 changes: 2 additions & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ port = 20000
# Number of JWT authentication attempts a client can fail before blocking that client temporarily from Signer access
# OPTIONAL, DEFAULT: 3
jwt_auth_fail_limit = 3
# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times
# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times.
# This also defines the interval at which failed attempts are regularly checked and expired ones are cleaned up.
# OPTIONAL, DEFAULT: 300
jwt_auth_fail_timeout_seconds = 300

Expand Down
36 changes: 9 additions & 27 deletions crates/common/src/commit/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use std::path::PathBuf;

use alloy::primitives::Address;
use eyre::WrapErr;
use reqwest::{
Certificate,
header::{AUTHORIZATION, HeaderMap, HeaderValue},
};
use reqwest::Certificate;
use serde::{Deserialize, Serialize};
use url::Url;

Expand Down Expand Up @@ -60,30 +57,13 @@ impl SignerClient {
Ok(Self { url: signer_server_url, client: builder.build()?, module_id, jwt_secret })
}

fn refresh_jwt(&mut self) -> Result<(), SignerClientError> {
let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?;

let mut auth_value =
HeaderValue::from_str(&format!("Bearer {jwt}")).wrap_err("invalid jwt")?;
auth_value.set_sensitive(true);

let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, auth_value);

self.client = reqwest::Client::builder()
.timeout(DEFAULT_REQUEST_TIMEOUT)
.default_headers(headers)
.build()?;

Ok(())
}

fn create_jwt_for_payload<T: Serialize>(
&mut self,
route: &str,
payload: &T,
) -> Result<Jwt, SignerClientError> {
let payload_vec = serde_json::to_vec(payload)?;
create_jwt(&self.module_id, &self.jwt_secret, Some(&payload_vec))
create_jwt(&self.module_id, &self.jwt_secret, route, Some(&payload_vec))
.wrap_err("failed to create JWT for payload")
.map_err(SignerClientError::JWTError)
}
Expand All @@ -92,10 +72,12 @@ impl SignerClient {
/// requested.
// TODO: add more docs on how proxy keys work
pub async fn get_pubkeys(&mut self) -> Result<GetPubkeysResponse, SignerClientError> {
self.refresh_jwt()?;
let jwt = create_jwt(&self.module_id, &self.jwt_secret, GET_PUBKEYS_PATH, None)
.wrap_err("failed to create JWT for payload")
.map_err(SignerClientError::JWTError)?;

let url = self.url.join(GET_PUBKEYS_PATH)?;
let res = self.client.get(url).send().await?;
let res = self.client.get(url).bearer_auth(jwt).send().await?;

if !res.status().is_success() {
return Err(SignerClientError::FailedRequest {
Expand All @@ -117,7 +99,7 @@ impl SignerClient {
Q: Serialize,
T: for<'de> Deserialize<'de>,
{
let jwt = self.create_jwt_for_payload(request)?;
let jwt = self.create_jwt_for_payload(route, request)?;

let url = self.url.join(route)?;
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
Expand Down Expand Up @@ -165,7 +147,7 @@ impl SignerClient {
where
T: ProxyId + for<'de> Deserialize<'de>,
{
let jwt = self.create_jwt_for_payload(request)?;
let jwt = self.create_jwt_for_payload(GENERATE_PROXY_KEY_PATH, request)?;

let url = self.url.join(GENERATE_PROXY_KEY_PATH)?;
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
Expand Down
9 changes: 4 additions & 5 deletions crates/common/src/config/mux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,12 @@ async fn fetch_lido_registry_keys(
let http = Http::with_client(client, rpc_url);
let is_local = http.guess_local();
let rpc_client = RpcClient::new(http, is_local);
let provider = ProviderBuilder::new().on_client(rpc_client);
let provider = ProviderBuilder::new().connect_client(rpc_client);

let registry_address = lido_registry_address(chain)?;
let registry = LidoRegistry::new(registry_address, provider);

let total_keys =
registry.getTotalSigningKeyCount(node_operator_id).call().await?._0.try_into()?;
let total_keys = registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?;

if total_keys == 0 {
return Ok(Vec::new());
Expand Down Expand Up @@ -449,7 +448,7 @@ mod tests {
#[tokio::test]
async fn test_lido_registry_address() -> eyre::Result<()> {
let url = Url::parse("https://ethereum-rpc.publicnode.com")?;
let provider = ProviderBuilder::new().on_http(url);
let provider = ProviderBuilder::new().connect_http(url);

let registry =
LidoRegistry::new(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5"), provider);
Expand All @@ -458,7 +457,7 @@ mod tests {
let node_operator_id = U256::from(1);

let total_keys: u64 =
registry.getTotalSigningKeyCount(node_operator_id).call().await?._0.try_into()?;
registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?;

assert!(total_keys > LIMIT as u64);

Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/config/pbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ impl PbsConfig {
if let Some(rpc_url) = &self.rpc_url {
// TODO: remove this once we support chain ids for custom chains
if !matches!(chain, Chain::Custom { .. }) {
let provider = ProviderBuilder::new().on_http(rpc_url.clone());
let provider = ProviderBuilder::new().connect_http(rpc_url.clone());
let chain_id = provider.get_chain_id().await?;
let chain_id_big = U256::from(chain_id);
ensure!(
Expand Down
3 changes: 2 additions & 1 deletion crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ pub struct SignerConfig {
pub jwt_auth_fail_limit: u32,

/// Duration in seconds to rate limit an endpoint after the JWT auth failure
/// limit has been reached
/// limit has been reached. This also defines the interval at which failed
/// attempts are regularly checked and expired ones are cleaned up.
#[serde(default = "default_u32::<SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT>")]
pub jwt_auth_fail_timeout_seconds: u32,

Expand Down
77 changes: 63 additions & 14 deletions crates/common/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ pub fn sign_commit_boost_root(

/// Verifies that a proposer commitment signature was generated by the given BLS
/// key for the provided message, chain ID, and module signing ID.
pub fn verify_proposer_commitment_signature_bls(
/// The message's Merkle root hash is used as the data.
pub fn verify_proposer_commitment_signature_bls_for_message(
chain: Chain,
pubkey: &BlsPublicKey,
msg: &impl TreeHash,
Expand All @@ -125,20 +126,31 @@ pub fn verify_proposer_commitment_signature_bls(
nonce: u64,
) -> bool {
let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN));
let object_root = types::PropCommitSigningInfo {
data: msg.tree_hash_root(),
module_signing_id: *module_signing_id,
nonce,
chain_id: chain.id(),
}
.tree_hash_root();
let object_root = get_object_root_from_msg(chain, msg, module_signing_id, nonce);
let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root();
verify_bls_signature(pubkey, signing_root, signature)
}

/// Verifies that a proposer commitment signature was generated by the given BLS
/// key for the provided message, chain ID, and module signing ID.
pub fn verify_proposer_commitment_signature_bls_for_data(
chain: Chain,
pubkey: &BlsPublicKey,
data: &B256,
signature: &BlsSignature,
module_signing_id: &B256,
nonce: u64,
) -> bool {
let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN));
let object_root = get_object_root_from_data(chain, data, module_signing_id, nonce);
let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root();
verify_bls_signature(pubkey, signing_root, signature)
}

/// Verifies that a proposer commitment signature was generated by the given
/// ECDSA key for the provided message, chain ID, and module signing ID.
pub fn verify_proposer_commitment_signature_ecdsa(
/// The message's Merkle root hash is used as the data.
pub fn verify_proposer_commitment_signature_ecdsa_for_message(
chain: Chain,
address: &Address,
msg: &impl TreeHash,
Expand All @@ -147,15 +159,52 @@ pub fn verify_proposer_commitment_signature_ecdsa(
nonce: u64,
) -> Result<(), eyre::Report> {
let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN));
let object_root = types::PropCommitSigningInfo {
data: msg.tree_hash_root(),
let object_root = get_object_root_from_msg(chain, msg, module_signing_id, nonce);
let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root();
verify_ecdsa_signature(address, &signing_root, signature)
}

/// Verifies that a proposer commitment signature was generated by the given
/// ECDSA key for the provided message, chain ID, and module signing ID.
pub fn verify_proposer_commitment_signature_ecdsa_for_data(
chain: Chain,
address: &Address,
data: &B256,
signature: &EcdsaSignature,
module_signing_id: &B256,
nonce: u64,
) -> Result<(), eyre::Report> {
let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN));
let object_root = get_object_root_from_data(chain, data, module_signing_id, nonce);
let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root();
verify_ecdsa_signature(address, &signing_root, signature)
}

/// Computes the object root for a proposer commitment by using the Merkle root
/// hash of the message as the data
fn get_object_root_from_msg<T: TreeHash>(
chain: Chain,
msg: &T,
module_signing_id: &B256,
nonce: u64,
) -> B256 {
get_object_root_from_data(chain, &msg.tree_hash_root(), module_signing_id, nonce)
}

/// Computes the object root for a proposer commitment using the raw data
fn get_object_root_from_data(
chain: Chain,
data: &B256,
module_signing_id: &B256,
nonce: u64,
) -> B256 {
types::PropCommitSigningInfo {
data: *data,
module_signing_id: *module_signing_id,
nonce,
chain_id: chain.id(),
}
.tree_hash_root();
let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root();
verify_ecdsa_signature(address, &signing_root, signature)
.tree_hash_root()
}

// ===============
Expand Down
Loading
Loading