From f1e6ff199f9f71b45cc3c138976716e1b9b17f7c Mon Sep 17 00:00:00 2001 From: David Petrov Date: Fri, 30 Aug 2024 11:35:33 +0300 Subject: [PATCH] feat(signer): add ECDSA proxy keys (#87) --- Cargo.toml | 3 +- README.md | 4 +- api/signer-api.yml | 143 ++++++--- bin/src/lib.rs | 6 +- config.example.toml | 17 +- crates/common/Cargo.toml | 1 + crates/common/src/commit/client.rs | 105 +++++-- crates/common/src/commit/request.rs | 124 ++++++-- crates/common/src/loader.rs | 24 +- crates/common/src/pbs/types/get_header.rs | 2 +- crates/common/src/signature.rs | 67 ++-- crates/common/src/signer.rs | 48 --- crates/common/src/signer/mod.rs | 8 + crates/common/src/signer/schemes/bls.rs | 103 +++++++ crates/common/src/signer/schemes/ecdsa.rs | 231 ++++++++++++++ crates/common/src/signer/schemes/mod.rs | 2 + crates/signer/Cargo.toml | 4 + crates/signer/src/error.rs | 10 +- crates/signer/src/manager.rs | 356 ++++++++++++++++------ crates/signer/src/service.rs | 49 ++- docs/docs/developing/commit-module.md | 27 +- examples/da_commit/src/main.rs | 56 ++-- tests/src/mock_relay.rs | 12 +- tests/tests/pbs_integration.rs | 20 +- 24 files changed, 1053 insertions(+), 369 deletions(-) delete mode 100644 crates/common/src/signer.rs create mode 100644 crates/common/src/signer/mod.rs create mode 100644 crates/common/src/signer/schemes/bls.rs create mode 100644 crates/common/src/signer/schemes/ecdsa.rs create mode 100644 crates/common/src/signer/schemes/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 4ada93cd..203ae372 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ blst = "0.3.11" tree_hash = "0.5" tree_hash_derive = "0.5" eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "9e12c21f268c80a3f002ae0ca27477f9f512eb6f" } +k256 = "0.13" # docker docker-compose-types = "0.12.0" @@ -79,4 +80,4 @@ dotenvy = "0.15.7" indexmap = "2.2.6" lazy_static = "1.5.0" bimap = { version = "0.6.3", features = ["serde"] } -derive_more = "0.99.18" +derive_more = { version = "1.0.0", features = ["from", "into", "deref", "display"] } diff --git a/README.md b/README.md index 1052ce5b..4249c4ae 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ async fn main() { let pubkey = *pubkeys.consensus.first().unwrap(); let datagram = Datagram { data: 42 }; - let request = SignRequest::builder(pubkey).with_msg(&datagram); + let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); let signature = config .signer_client - .request_signature(&request) + .request_consensus_signature(&request) .await .unwrap(); diff --git a/api/signer-api.yml b/api/signer-api.yml index 1cccb891..e03331e6 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -15,7 +15,7 @@ paths: - BearerAuth: [] responses: "200": - description: A list of Bls pubkeys + description: "All public keys available to the module: consensus pubkeys (BLS) and proxy pubkeys (BLS and ECDSA)" content: application/json: schema: @@ -25,18 +25,17 @@ paths: description: Consensus validator pubkeys type: array items: - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - proxy: - description: Proxy validator pubkeys + $ref: '#/components/schemas/BlsPubkey' + proxy_bls: + description: BLS proxy validator pubkeys type: array items: - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + $ref: '#/components/schemas/BlsPubkey' + proxy_ecdsa: + description: ECDSA proxy validator pubkeys + type: array + items: + $ref: '#/components/schemas/EcdsaPubkey' "500": description: Internal error content: @@ -67,34 +66,55 @@ paths: application/json: schema: type: object + required: [type, pubkey, object_root] properties: - pubkey: - description: BLS public key of validator + type: + description: Type of the sign request type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - is_proxy: - description: Whether the request is for a proxy pubkey - type: boolean - example: false + enum: [consensus, proxy_bls, proxy_ecdsa] + pubkey: + description: Public key of the validator + oneOf: + - $ref: '#/components/schemas/BlsPubkey' + - $ref: '#/components/schemas/EcdsaPubkey' object_root: description: The root of the object to be signed type: string format: hex pattern: "^0x[a-fA-F0-9]{64}$" example: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + examples: + Consensus: + value: + type: "consensus" + pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + ProxyBls: + value: + type: "proxy_bls" + pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + ProxyEcdsa: + value: + type: "proxy_ecdsa" + pubkey: "0x023b2806b1b1dfa34dd90b01546906cef3e4c8e0fc0cba60480e9eb4d0a0828311" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" responses: "200": - description: Successs + description: Success content: application/json: schema: - type: string - description: The validator signature - format: hex - pattern: "^0x[a-fA-F0-9]{192}$" - example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + oneOf: + - $ref: '#/components/schemas/BlsSignature' + - $ref: '#/components/schemas/EcdsaSignature' + examples: + Consensus: + value: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + ProxyBls: + value: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + ProxyEcdsa: + value: "0xe6a0c0c41a6d4af9794882c18c5280376cbfb7921453612dea02ed8f47b1208455f07931dc12c4b70c4e8ae216db0136000ec2cf17244189f012de356ac46cec" "404": description: Unknown value (pubkey, etc.) content: @@ -141,13 +161,25 @@ paths: application/json: schema: type: object + required: [pubkey, scheme] properties: pubkey: description: a validator BLS public key for which to generate a proxy key + allOf: + - $ref: '#/components/schemas/BlsPubkey' + scheme: + description: signature scheme to generate proxy keypair for type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0xac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118" + enum: [bls, ecdsa] + examples: + Bls: + value: + pubkey: "0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a" + scheme: "bls" + Ecdsa: + value: + pubkey: "0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a" + scheme: "ecdsa" responses: "200": description: Successs @@ -161,22 +193,30 @@ paths: properties: delegator: description: the validator BLS public key for which the proxy key was generated (the same one as requested) - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0xac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118" + allOf: + - $ref: '#/components/schemas/BlsPubkey' proxy: description: the generated proxy public key - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{96}$" - example: "0x8a481a7a51c430a9bafa64366bc4934f5880f5f1d97646f91680936a53f2a268fdde5369430a2b4bb700c5f82cfbab3f" + oneOf: + - $ref: '#/components/schemas/BlsPubkey' + - $ref: '#/components/schemas/EcdsaPubkey' signature: description: The signature of the proxy delegation - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{192}$" - example: "0xabfacf1cd17d80abfc6fa6b8e534ab25cdb1f95a855706ef604672c8695401a84c7834008e57925d4259c551b7c03d1a16f05b082294fadcba802a61a5cccfb5e96dd1dce4c9dac3f6d15254495019146346670be1f374a67cb0cda2aaf72d00" + allOf: + - $ref: '#/components/schemas/BlsSignature' + examples: + Bls: + value: + message: + delegator: "0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a" + proxy: "0xb646318d81b7cff3f8aae5040eab11927b4a99542c02970a1ab8069a83e5b76b302705d0b5e0054831ce2af72088bf30" + signature: "0x88274f2d78d30ae429cc16f5c64657b491ccf26291c821cf953da34f16d60947d4f245decdce4a492e8d8f949482051b184aaa890d5dd97788387689335a1fee37cbe55c0227f81b073ce6e93b45f96169f497ed322d3d384d79ccaa7846d5ab" + Ecdsa: + value: + message: + delegator: "0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a" + proxy: "0x023b2806b1b1dfa34dd90b01546906cef3e4c8e0fc0cba60480e9eb4d0a0828311" + signature: "0xb5b5b71d1701cc45086af3d3d86bf9d3c509442835e5b9f7734923edc9a6c538e743d70613cdef90b7e5b171fbbe6a29075b3f155e4bd66d81ff9dbc3b6d7fa677d169b2ceab727ffa079a31fe1fc0e478752e9da9566a9408e4db24ac6104db" "404": description: Unknown value (pubkey, etc.) content: @@ -216,3 +256,24 @@ components: type: http scheme: bearer bearerFormat: JWT + schemas: + BlsPubkey: + type: string + format: hex + pattern: "^0x[a-fA-F0-9]{96}$" + example: "0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a" + EcdsaPubkey: + type: string + format: hex + pattern: "^0x[a-fA-F0-9]{64}$" + example: "0x023b2806b1b1dfa34dd90b01546906cef3e4c8e0fc0cba60480e9eb4d0a0828311" + BlsSignature: + type: string + format: hex + pattern: "^0x[a-fA-F0-9]{192}$" + example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + EcdsaSignature: + type: string + format: hex + pattern: "^0x[a-fA-F0-9]{128}$" + example: "0xe6a0c0c41a6d4af9794882c18c5280376cbfb7921453612dea02ed8f47b1208455f07931dc12c4b70c4e8ae216db0136000ec2cf17244189f012de356ac46cec" diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 81be3e30..eb9f5067 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -1,12 +1,16 @@ pub mod prelude { pub use cb_common::{ commit, - commit::request::{SignRequest, SignedProxyDelegation}, + commit::request::{ + SignConsensusRequest, SignProxyRequest, SignedProxyDelegation, + SignedProxyDelegationBls, SignedProxyDelegationEcdsa, + }, config::{ load_builder_module_config, load_commit_module_config, load_pbs_config, load_pbs_custom_config, StartCommitModuleConfig, }, pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent}, + signer::{BlsPublicKey, BlsSignature, EcdsaPublicKey, EcdsaSignature}, utils::{ initialize_pbs_tracing_log, initialize_tracing_log, utcnow_ms, utcnow_ns, utcnow_sec, utcnow_us, diff --git a/config.example.toml b/config.example.toml index 95633156..d067c466 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,4 +1,4 @@ -# The main configuration file for the Commit-Boost sidecar. +# The main configuration file for the Commit-Boost sidecar. # Some fields are optional and can be omitted, in which case the default value, if present, will be used. # Chain spec id. Supported values: Mainnet, Holesky, Helder @@ -18,7 +18,7 @@ port = 18550 # Whether to forward `status` calls to relays or skip and return 200 # OPTIONAL, DEFAULT: true relay_check = true -# Timeout in milliseconds for the `get_header` call to relays. Note that the CL has also a timeout (e.g. 1 second) so +# Timeout in milliseconds for the `get_header` call to relays. Note that the CL has also a timeout (e.g. 1 second) so # this should be lower than that, leaving some margin for overhead # OPTIONAL, DEFAULT: 950 timeout_get_header_ms = 950 @@ -34,10 +34,11 @@ skip_sigverify = false # Minimum bid in ETH that will be accepted from `get_header` # OPTIONAL, DEFAULT: 0.0 min_bid_eth = 0.0 +# How late in milliseconds in the slot is "late". This impacts the `get_header` requests, by shortening timeouts for `get_header` calls to # List of URLs of relay monitors to send registrations to # OPTIONAL relay_monitors = [] -# How late in milliseconds in the slot is "late". This impacts the `get_header` requests, by shortening timeouts for `get_header` calls to +# How late in milliseconds in the slot is "late". This impacts the `get_header` requests, by shortening timeouts for `get_header` calls to # relays and make sure a header is returned within this deadline. If the request from the CL comes later in the slot, then fetching headers is skipped # to force local building and miniminzing the risk of missed slots. See also the timing games section below # OPTIONAL, DEFAULT: 2000 @@ -55,12 +56,12 @@ url = "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb headers = { X-MyCustomHeader = "MyCustomValue" } # Whether to enable timing games, as tuned by `target_first_request_ms` and `frequency_get_header_ms`. # These values should be carefully chosen for each relay, as each relay has different latency and timing games setups. -# They should only be used by advanced users, and if mis-configured can result in unforeseen effects, e.g. fetching a lower header value, +# They should only be used by advanced users, and if mis-configured can result in unforeseen effects, e.g. fetching a lower header value, # or getting a temporary IP ban. -# +# # EXAMPLES # Assuming: timeout_get_header_ms = 950, frequency_get_header_ms = 300, target_first_request_ms = 200, late_in_slot_time_ms = 2000 -# +# # 1) CL request comes at 100ms in the slot (max timeout 1050ms in the slot), then: # - sleep for 100ms # - send request at 200ms with 850ms timeout @@ -112,13 +113,13 @@ id = "DA_COMMIT" type = "commit" # Docker image of the module docker_image = "test_da_commit" -# Additional config needed by the business logic of the module should also be set here. +# Additional config needed by the business logic of the module should also be set here. # See also `examples/da_commit/src/main.rs` for more information sleep_secs = 5 # Configuration for how metrics should be collected and scraped [metrics] -# Path to a `prometheus.yml` file to use in Prometheus. If using a custom config file, be sure to add a +# Path to a `prometheus.yml` file to use in Prometheus. If using a custom config file, be sure to add a # file discovery section as follows: # ```yml # file_sd_configs: diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 61b295aa..ec2b9a9f 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -35,6 +35,7 @@ blst.workspace = true tree_hash.workspace = true tree_hash_derive.workspace = true eth2_keystore.workspace = true +k256.workspace = true # misc thiserror.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index e7c65979..b61fd42c 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,22 +1,25 @@ use std::sync::Arc; -use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; +use alloy::rpc::types::beacon::BlsSignature; use eyre::WrapErr; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use super::{ constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH}, error::SignerClientError, - request::{GenerateProxyRequest, SignRequest, SignedProxyDelegation}, + request::{ + EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, PublicKey, + SignConsensusRequest, SignProxyRequest, SignRequest, SignedProxyDelegation, + }, +}; +use crate::{ + signer::{ + schemes::{bls::BlsPublicKey, ecdsa::EcdsaSignature}, + EcdsaPublicKey, + }, + DEFAULT_REQUEST_TIMEOUT, }; -use crate::DEFAULT_REQUEST_TIMEOUT; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GetPubkeysResponse { - pub consensus: Vec, - pub proxy: Vec, -} /// Client used by commit modules to request signatures via the Signer API #[derive(Debug, Clone)] @@ -45,31 +48,26 @@ impl SignerClient { } /// Request a list of validator pubkeys for which signatures can be - /// requested. TODO: add more docs on how proxy keys work + /// requested. + // TODO: add more docs on how proxy keys work pub async fn get_pubkeys(&self) -> Result { - let url = format!("{}{}", self.url, GET_PUBKEYS_PATH); - let res = self.client.get(&url).send().await?; + let res = self.client.get(&format!("{}{}", self.url, GET_PUBKEYS_PATH)).send().await?; - let status = res.status(); - let response_bytes = res.bytes().await?; - - if !status.is_success() { + if !res.status().is_success() { return Err(SignerClientError::FailedRequest { - status: status.as_u16(), - error_msg: String::from_utf8_lossy(&response_bytes).into_owned(), + status: res.status().as_u16(), + error_msg: String::from_utf8_lossy(&res.bytes().await?).into_owned(), }); } - let parsed_response: GetPubkeysResponse = serde_json::from_slice(&response_bytes)?; - - Ok(parsed_response) + Ok(serde_json::from_slice(&res.bytes().await?)?) } /// Send a signature request - pub async fn request_signature( - &self, - request: &SignRequest, - ) -> Result { + async fn request_signature(&self, request: &SignRequest) -> Result + where + T: for<'de> Deserialize<'de>, + { let url = format!("{}{}", self.url, REQUEST_SIGNATURE_PATH); let res = self.client.post(&url).json(&request).send().await?; @@ -83,17 +81,40 @@ impl SignerClient { }); } - let signature: BlsSignature = serde_json::from_slice(&response_bytes)?; + let signature = serde_json::from_slice(&response_bytes)?; Ok(signature) } - pub async fn generate_proxy_key( + pub async fn request_consensus_signature( + &self, + request: SignConsensusRequest, + ) -> Result { + self.request_signature(&request.into()).await + } + + pub async fn request_proxy_signature_ecdsa( &self, - pubkey: BlsPublicKey, - ) -> Result { + request: SignProxyRequest, + ) -> Result { + self.request_signature(&request.into()).await + } + + pub async fn request_proxy_signature_bls( + &self, + request: SignProxyRequest, + ) -> Result { + self.request_signature(&request.into()).await + } + + async fn generate_proxy_key( + &self, + request: &GenerateProxyRequest, + ) -> Result, SignerClientError> + where + T: PublicKey + for<'de> Deserialize<'de>, + { let url = format!("{}{}", self.url, GENERATE_PROXY_KEY_PATH); - let request = GenerateProxyRequest::new(pubkey); let res = self.client.post(&url).json(&request).send().await?; let status = res.status(); @@ -110,4 +131,26 @@ impl SignerClient { Ok(signed_proxy_delegation) } + + pub async fn generate_proxy_key_bls( + &self, + consensus_pubkey: BlsPublicKey, + ) -> Result, SignerClientError> { + let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Bls); + + let bls_signed_proxy_delegation = self.generate_proxy_key(&request).await?; + + Ok(bls_signed_proxy_delegation) + } + + pub async fn generate_proxy_key_ecdsa( + &self, + consensus_pubkey: BlsPublicKey, + ) -> Result, SignerClientError> { + let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Ecdsa); + + let ecdsa_signed_proxy_delegation = self.generate_proxy_key(&request).await?; + + Ok(ecdsa_signed_proxy_delegation) + } } diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 676c8d9f..df6ba8e7 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -1,27 +1,58 @@ -use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; +use std::fmt::{self, Debug, Display, LowerHex}; + +use alloy::rpc::types::beacon::BlsSignature; +use derive_more::derive::From; use serde::{Deserialize, Serialize}; +use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -use crate::{error::BlstErrorWrapper, signature::verify_signed_builder_message, types::Chain}; +use crate::{ + error::BlstErrorWrapper, + signature::verify_signed_builder_message, + signer::schemes::{bls::BlsPublicKey, ecdsa::EcdsaPublicKey}, + types::Chain, +}; -// TODO: might need to adapt the SignedProxyDelegation so that it goes through -// web3 signer +pub trait PublicKey: + AsRef<[u8]> + Debug + Clone + Copy + Encode + Decode + TreeHash + Display + LowerHex +{ +} + +impl PublicKey for EcdsaPublicKey {} + +impl PublicKey for BlsPublicKey {} + +// GENERIC PROXY DELEGATION #[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, TreeHash)] -pub struct ProxyDelegation { +pub struct ProxyDelegation { pub delegator: BlsPublicKey, - pub proxy: BlsPublicKey, + pub proxy: T, +} + +pub type ProxyDelegationBls = ProxyDelegation; +pub type ProxyDelegationEcdsa = ProxyDelegation; + +impl fmt::Display for ProxyDelegation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Delegator: {}\nProxy: {}", self.delegator, self.proxy) + } } +// TODO: might need to adapt the SignedProxyDelegation so that it goes through +// web3 signer #[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct SignedProxyDelegation { - pub message: ProxyDelegation, +pub struct SignedProxyDelegation { + pub message: ProxyDelegation, /// Signature of message with the delegator keypair pub signature: BlsSignature, } -impl SignedProxyDelegation { +pub type SignedProxyDelegationBls = SignedProxyDelegation; +pub type SignedProxyDelegationEcdsa = SignedProxyDelegation; + +impl SignedProxyDelegation { pub fn validate(&self, chain: Chain) -> Result<(), BlstErrorWrapper> { verify_signed_builder_message( chain, @@ -32,24 +63,60 @@ impl SignedProxyDelegation { } } +impl fmt::Display for SignedProxyDelegation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}\nSignature: {}", self.message, self.signature) + } +} + +// TODO(David): This struct shouldn't be visible to module authors +#[derive(Debug, Clone, Serialize, Deserialize, From)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum SignRequest { + Consensus(SignConsensusRequest), + ProxyBls(SignProxyRequest), + ProxyEcdsa(SignProxyRequest), +} + #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignRequest { +pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, - pub is_proxy: bool, + #[serde(with = "alloy::hex::serde")] pub object_root: [u8; 32], } -impl SignRequest { - pub fn new(pubkey: BlsPublicKey, is_proxy: bool, object_root: [u8; 32]) -> SignRequest { - Self { pubkey, is_proxy, object_root } +impl SignConsensusRequest { + pub fn new(pubkey: BlsPublicKey, object_root: [u8; 32]) -> Self { + Self { pubkey, object_root } } pub fn builder(pubkey: BlsPublicKey) -> Self { - Self::new(pubkey, false, [0; 32]) + Self::new(pubkey, [0; 32]) + } + + pub fn with_root(self, object_root: [u8; 32]) -> Self { + Self { object_root, ..self } + } + + pub fn with_msg(self, msg: &impl TreeHash) -> Self { + self.with_root(msg.tree_hash_root().0) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignProxyRequest { + pub pubkey: T, + #[serde(with = "alloy::hex::serde")] + pub object_root: [u8; 32], +} + +impl SignProxyRequest { + pub fn new(pubkey: T, object_root: [u8; 32]) -> Self { + Self { pubkey, object_root } } - pub fn is_proxy(self) -> Self { - Self { is_proxy: true, ..self } + pub fn builder(pubkey: T) -> Self { + Self::new(pubkey, [0; 32]) } pub fn with_root(self, object_root: [u8; 32]) -> Self { @@ -61,13 +128,30 @@ impl SignRequest { } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum EncryptionScheme { + #[serde(rename = "bls")] + Bls, + #[serde(rename = "ecdsa")] + Ecdsa, +} + +// TODO(David): This struct shouldn't be visible to module authors #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerateProxyRequest { - pub pubkey: BlsPublicKey, + pub consensus_pubkey: BlsPublicKey, + pub scheme: EncryptionScheme, } impl GenerateProxyRequest { - pub fn new(pubkey: BlsPublicKey) -> Self { - GenerateProxyRequest { pubkey } + pub fn new(consensus_pubkey: BlsPublicKey, scheme: EncryptionScheme) -> Self { + GenerateProxyRequest { consensus_pubkey, scheme } } } + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GetPubkeysResponse { + pub consensus: Vec, + pub proxy_bls: Vec, + pub proxy_ecdsa: Vec, +} diff --git a/crates/common/src/loader.rs b/crates/common/src/loader.rs index cf5d453a..3450506f 100644 --- a/crates/common/src/loader.rs +++ b/crates/common/src/loader.rs @@ -2,12 +2,12 @@ use std::fs; use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey}; use eth2_keystore::Keystore; -use eyre::eyre; +use eyre::{eyre, Context}; use serde::{de, Deserialize, Deserializer, Serialize}; use crate::{ config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV}, - signer::Signer, + signer::ConsensusSigner, }; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -24,12 +24,12 @@ pub enum SignerLoader { } impl SignerLoader { - pub fn load_keys(self) -> eyre::Result> { + pub fn load_keys(self) -> eyre::Result> { // TODO: add flag to support also native loader self.load_from_env() } - pub fn load_from_env(self) -> eyre::Result> { + pub fn load_from_env(self) -> eyre::Result> { Ok(match self { SignerLoader::File { .. } => { let path = load_env_var(SIGNER_KEYS_ENV)?; @@ -39,15 +39,16 @@ impl SignerLoader { let keys: Vec = serde_json::from_str(&file)?; keys.into_iter() - .map(|k| Signer::new_from_bytes(&k.secret_key)) - .collect::>>()? + .map(|k| ConsensusSigner::new_from_bytes(&k.secret_key)) + .collect::>() + .context("failed to load signers")? } SignerLoader::ValidatorsDir { .. } => { // TODO: hacky way to load for now, we should support reading the // definitions.yml file let keys_path = load_env_var(SIGNER_DIR_KEYS_ENV)?; let secrets_path = load_env_var(SIGNER_DIR_SECRETS_ENV)?; - load_secrets_and_keys(keys_path, secrets_path).expect("failed to load signers") + load_secrets_and_keys(keys_path, secrets_path).context("failed to load signers")? } }) } @@ -71,7 +72,10 @@ impl<'de> Deserialize<'de> for FileKey { } } -fn load_secrets_and_keys(keys_path: String, secrets_path: String) -> eyre::Result> { +fn load_secrets_and_keys( + keys_path: String, + secrets_path: String, +) -> eyre::Result> { let entries = fs::read_dir(keys_path.clone())?; let mut signers = Vec::new(); @@ -98,12 +102,12 @@ fn load_secrets_and_keys(keys_path: String, secrets_path: String) -> eyre::Resul Ok(signers) } -fn load_one(ks_path: String, pw_path: String) -> eyre::Result { +fn load_one(ks_path: String, pw_path: String) -> eyre::Result { let keystore = Keystore::from_json_file(ks_path).map_err(|_| eyre!("failed reading json"))?; let password = fs::read(pw_path)?; let key = keystore.decrypt_keypair(&password).map_err(|_| eyre!("failed decrypting keypair"))?; - Signer::new_from_bytes(key.sk.serialize().as_bytes()) + ConsensusSigner::new_from_bytes(key.sk.serialize().as_bytes()) } #[cfg(test)] diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index 531e2dd4..c00c2404 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -117,7 +117,7 @@ mod tests { assert!(verify_signed_builder_message( Chain::Holesky, - &parsed.message.pubkey, + &parsed.message.pubkey.into(), &parsed.message, &parsed.signature, ) diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index f5262fb3..2e6a40dd 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -1,9 +1,4 @@ use alloy::rpc::types::beacon::{constants::BLS_DST_SIG, BlsPublicKey, BlsSignature}; -use blst::{ - min_pk::{PublicKey, SecretKey, Signature}, - BLST_ERROR, -}; -use rand::RngCore; use ssz_derive::{Decode, Encode}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; @@ -11,62 +6,34 @@ use tree_hash_derive::TreeHash; use crate::{ constants::{APPLICATION_BUILDER_DOMAIN, GENESIS_VALIDATORS_ROOT}, error::BlstErrorWrapper, + signer::{schemes::bls::verify_bls_signature, BlsSecretKey}, types::Chain, - utils::{alloy_pubkey_to_blst, alloy_sig_to_blst}, }; -pub fn random_secret() -> SecretKey { - let mut rng = rand::thread_rng(); - let mut ikm = [0u8; 32]; - rng.fill_bytes(&mut ikm); - - match SecretKey::key_gen(&ikm, &[]) { - Ok(key) => key, - // Key material is always valid (32 `u8`s), so `key_gen` can't return Err. - Err(_) => unreachable!(), - } -} - -pub fn verify_signature( - pubkey: &BlsPublicKey, - msg: &[u8], - signature: &BlsSignature, -) -> Result<(), BlstErrorWrapper> { - let pubkey: PublicKey = alloy_pubkey_to_blst(pubkey)?; - let signature: Signature = alloy_sig_to_blst(signature)?; - - let res = signature.verify(true, msg, BLS_DST_SIG, &[], &pubkey, true); - if res == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(res.into()) - } -} - -pub fn sign_message(secret_key: &SecretKey, msg: &[u8]) -> BlsSignature { +pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { let signature = secret_key.sign(msg, BLS_DST_SIG, &[]).to_bytes(); BlsSignature::from_slice(&signature) } -#[derive(Default, Debug, Encode, Decode, TreeHash)] -struct SigningData { - object_root: [u8; 32], - signing_domain: [u8; 32], -} - pub fn compute_signing_root(object_root: [u8; 32], signing_domain: [u8; 32]) -> [u8; 32] { + #[derive(Default, Debug, Encode, Decode, TreeHash)] + struct SigningData { + object_root: [u8; 32], + signing_domain: [u8; 32], + } + let signing_data = SigningData { object_root, signing_domain }; signing_data.tree_hash_root().0 } -#[derive(Debug, Encode, Decode, TreeHash)] -struct ForkData { - fork_version: [u8; 4], - genesis_validators_root: [u8; 32], -} - #[allow(dead_code)] fn compute_builder_domain(chain: Chain) -> [u8; 32] { + #[derive(Debug, Encode, Decode, TreeHash)] + struct ForkData { + fork_version: [u8; 4], + genesis_validators_root: [u8; 32], + } + let mut domain = [0u8; 32]; domain[..4].copy_from_slice(&APPLICATION_BUILDER_DOMAIN); @@ -88,12 +55,12 @@ pub fn verify_signed_builder_message( let domain = chain.builder_domain(); let signing_root = compute_signing_root(msg.tree_hash_root().0, domain); - verify_signature(pubkey, &signing_root, signature) + verify_bls_signature(pubkey, &signing_root, signature) } pub fn sign_builder_message( chain: Chain, - secret_key: &SecretKey, + secret_key: &BlsSecretKey, msg: &impl TreeHash, ) -> BlsSignature { sign_builder_root(chain, secret_key, msg.tree_hash_root().0) @@ -101,7 +68,7 @@ pub fn sign_builder_message( pub fn sign_builder_root( chain: Chain, - secret_key: &SecretKey, + secret_key: &BlsSecretKey, object_root: [u8; 32], ) -> BlsSignature { let domain = chain.builder_domain(); diff --git a/crates/common/src/signer.rs b/crates/common/src/signer.rs deleted file mode 100644 index 15eb5b51..00000000 --- a/crates/common/src/signer.rs +++ /dev/null @@ -1,48 +0,0 @@ -use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; -use blst::min_pk::SecretKey; -use eyre::Result; -use tree_hash::TreeHash; - -use crate::{ - error::BlstErrorWrapper, - signature::{random_secret, sign_builder_message}, - types::Chain, - utils::blst_pubkey_to_alloy, -}; - -#[derive(Clone)] -pub enum Signer { - Local(SecretKey), -} - -impl Signer { - pub fn new_random() -> Self { - Signer::Local(random_secret()) - } - - pub fn new_from_bytes(bytes: &[u8]) -> Result { - let secret_key = SecretKey::from_bytes(bytes).map_err(BlstErrorWrapper::from)?; - Ok(Self::Local(secret_key)) - } - - pub fn pubkey(&self) -> BlsPublicKey { - match self { - Signer::Local(secret) => blst_pubkey_to_alloy(&secret.sk_to_pk()), - } - } - - pub async fn sign(&self, chain: Chain, object_root: &[u8; 32]) -> BlsSignature { - match self { - Signer::Local(sk) => sign_builder_message(chain, sk, object_root), - } - } - - pub async fn sign_msg(&self, chain: Chain, msg: &impl TreeHash) -> BlsSignature { - match self { - Signer::Local(sk) => { - let object_root = msg.tree_hash_root(); - sign_builder_message(chain, sk, &object_root.0) - } - } - } -} diff --git a/crates/common/src/signer/mod.rs b/crates/common/src/signer/mod.rs new file mode 100644 index 00000000..3622e1c1 --- /dev/null +++ b/crates/common/src/signer/mod.rs @@ -0,0 +1,8 @@ +pub mod schemes; + +pub use schemes::{ + bls::{BlsPublicKey, BlsSecretKey, BlsSignature, BlsSigner}, + ecdsa::{EcdsaPublicKey, EcdsaSecretKey, EcdsaSignature, EcdsaSigner}, +}; + +pub type ConsensusSigner = BlsSigner; diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs new file mode 100644 index 00000000..00911766 --- /dev/null +++ b/crates/common/src/signer/schemes/bls.rs @@ -0,0 +1,103 @@ +pub use alloy::rpc::types::beacon::BlsSignature; +use alloy::rpc::types::beacon::{constants::BLS_DST_SIG, BlsPublicKey as BlsPublicKeyInner}; +use blst::BLST_ERROR; +use derive_more::derive::{Deref, Display, From, Into, LowerHex}; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; + +use crate::{ + error::BlstErrorWrapper, signature::sign_builder_root, types::Chain, + utils::blst_pubkey_to_alloy, +}; + +pub type BlsSecretKey = blst::min_pk::SecretKey; + +// TODO(David): +// This wrapper type is potentially a temporary solution, merely to implement +// `TreeHash`. Remove when progress is made on this issue (https://github.com/sigp/tree_hash/issues/22) +// or refine the boundaries between our wrapper `BlsPublicKey` type +// and alloy's `BlsPublicKey` if we stick with it + +// std traits +#[derive(Debug, Clone, Copy, LowerHex, Display, PartialEq, Eq, Hash, Default)] +// serde, ssz, tree_hash +#[derive(Serialize, Deserialize, Encode, Decode, TreeHash)] +#[ssz(struct_behaviour = "transparent")] +#[serde(transparent)] +// derive_more +#[derive(Deref, From, Into)] +pub struct BlsPublicKey { + inner: BlsPublicKeyInner, +} + +impl AsRef<[u8]> for BlsPublicKey { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +#[derive(Clone)] +pub enum BlsSigner { + Local(BlsSecretKey), +} + +impl BlsSigner { + pub fn new_random() -> Self { + Self::Local(random_secret()) + } + + pub fn new_from_bytes(bytes: &[u8]) -> eyre::Result { + let secret = BlsSecretKey::from_bytes(bytes).map_err(BlstErrorWrapper::from)?; + Ok(Self::Local(secret)) + } + + pub fn pubkey(&self) -> BlsPublicKey { + match self { + BlsSigner::Local(secret) => blst_pubkey_to_alloy(&secret.sk_to_pk()).into(), + } + } + + pub async fn sign(&self, chain: Chain, object_root: [u8; 32]) -> BlsSignature { + match self { + BlsSigner::Local(sk) => sign_builder_root(chain, sk, object_root), + } + } + + pub async fn sign_msg(&self, chain: Chain, msg: &impl TreeHash) -> BlsSignature { + self.sign(chain, msg.tree_hash_root().0).await + } +} + +fn random_secret() -> BlsSecretKey { + use rand::RngCore; + + let mut rng = rand::thread_rng(); + let mut ikm = [0u8; 32]; + rng.fill_bytes(&mut ikm); + + match BlsSecretKey::key_gen(&ikm, &[]) { + Ok(key) => key, + // Key material is always valid (32 `u8`s), so `key_gen` can't return Err. + Err(_) => unreachable!(), + } +} + +pub fn verify_bls_signature( + pubkey: &BlsPublicKeyInner, + msg: &[u8], + signature: &BlsSignature, +) -> Result<(), BlstErrorWrapper> { + use crate::utils::{alloy_pubkey_to_blst, alloy_sig_to_blst}; + + let pubkey = alloy_pubkey_to_blst(pubkey)?; + let signature = alloy_sig_to_blst(signature)?; + + let res = signature.verify(true, msg, BLS_DST_SIG, &[], &pubkey, true); + if res == BLST_ERROR::BLST_SUCCESS { + Ok(()) + } else { + Err(res.into()) + } +} diff --git a/crates/common/src/signer/schemes/ecdsa.rs b/crates/common/src/signer/schemes/ecdsa.rs new file mode 100644 index 00000000..359a3054 --- /dev/null +++ b/crates/common/src/signer/schemes/ecdsa.rs @@ -0,0 +1,231 @@ +use core::fmt; +use std::hash::Hash; + +use derive_more::derive::{Deref, From, Into}; +use k256::{ + ecdsa::{Signature as EcdsaSignatureInner, VerifyingKey as EcdsaPublicKeyInner}, + elliptic_curve::generic_array::GenericArray, +}; +use serde::{Deserialize, Serialize}; +use serde_utils::hex; +use ssz_types::{ + typenum::{U33, U64}, + FixedVector, +}; +use tree_hash::TreeHash; + +use crate::{signature::compute_signing_root, types::Chain}; + +pub type EcdsaSecretKey = k256::ecdsa::SigningKey; + +type CompressedPublicKey = [u8; 33]; + +#[derive(Debug, Clone, Copy, From, Into, Serialize, Deserialize, PartialEq, Eq, Deref, Hash)] +#[serde(transparent)] +pub struct EcdsaPublicKey { + #[serde(with = "alloy::hex::serde")] + encoded: CompressedPublicKey, +} + +impl EcdsaPublicKey { + /// Size of the public key in bytes. We store the SEC1 encoded affine point + /// compressed, thus 33 bytes. + const SIZE: usize = 33; +} + +impl Default for EcdsaPublicKey { + fn default() -> Self { + Self { encoded: [0; Self::SIZE] } + } +} + +impl TreeHash for EcdsaPublicKey { + fn tree_hash_type() -> tree_hash::TreeHashType { + tree_hash::TreeHashType::Vector + } + + fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding { + unreachable!("Vector should never be packed.") + } + + fn tree_hash_packing_factor() -> usize { + unreachable!("Vector should never be packed.") + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + // NOTE: + // Unnecessary copying into a `FixedVector` just for its `tree_hash_root` + // implementation. If this becomes a performance issue, we could use + // `ssz_types::tree_hash::vec_tree_hash_root`, which is unfortunately + // not public. + let vec = self.encoded.to_vec(); + FixedVector::::from(vec).tree_hash_root() + } +} + +impl ssz::Encode for EcdsaPublicKey { + #[inline] + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_bytes_len(&self) -> usize { + Self::SIZE + } + + #[inline] + fn ssz_fixed_len() -> usize { + Self::SIZE + } + + #[inline] + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.encoded) + } + + #[inline] + fn as_ssz_bytes(&self) -> Vec { + self.encoded.to_vec() + } +} + +impl ssz::Decode for EcdsaPublicKey { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + Self::SIZE + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let encoded = <[u8; 33]>::try_from(bytes).map_err(|_| { + ssz::DecodeError::InvalidByteLength { len: bytes.len(), expected: Self::SIZE } + })?; + + Ok(EcdsaPublicKey { encoded }) + } +} + +impl From for EcdsaPublicKey { + fn from(value: EcdsaPublicKeyInner) -> Self { + let encoded: [u8; Self::SIZE] = value.to_encoded_point(true).as_bytes().try_into().unwrap(); + + EcdsaPublicKey { encoded } + } +} + +impl AsRef<[u8]> for EcdsaPublicKey { + fn as_ref(&self) -> &[u8] { + &self.encoded + } +} + +impl fmt::LowerHex for EcdsaPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.as_ref()))?; + Ok(()) + } +} + +impl fmt::Display for EcdsaPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:x}") + } +} + +#[derive(Clone, Deref, Serialize, Deserialize)] +#[serde(transparent)] +pub struct EcdsaSignature { + #[serde(with = "alloy::hex::serde")] + encoded: [u8; 64], +} + +impl Default for EcdsaSignature { + fn default() -> Self { + Self { encoded: [0; 64] } + } +} + +impl From for EcdsaSignature { + fn from(value: EcdsaSignatureInner) -> Self { + Self { encoded: value.to_bytes().as_slice().try_into().unwrap() } + } +} + +impl AsRef<[u8]> for EcdsaSignature { + fn as_ref(&self) -> &[u8] { + &self.encoded + } +} + +impl TryFrom<&[u8]> for EcdsaSignature { + type Error = k256::ecdsa::Error; + + fn try_from(value: &[u8]) -> std::result::Result { + Ok(EcdsaSignatureInner::from_slice(value)?.into()) + } +} + +impl fmt::LowerHex for EcdsaSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.as_ref()))?; + Ok(()) + } +} + +impl fmt::Display for EcdsaSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:x}") + } +} + +// SIGNER +#[derive(Clone)] +pub enum EcdsaSigner { + Local(EcdsaSecretKey), +} + +impl EcdsaSigner { + pub fn new_random() -> Self { + Self::Local(EcdsaSecretKey::random(&mut rand::thread_rng())) + } + + pub fn new_from_bytes(bytes: &[u8]) -> eyre::Result { + let secret = EcdsaSecretKey::from_slice(bytes)?; + Ok(Self::Local(secret)) + } + + pub fn pubkey(&self) -> EcdsaPublicKey { + match self { + EcdsaSigner::Local(secret) => EcdsaPublicKeyInner::from(secret).into(), + } + } + + pub async fn sign(&self, chain: Chain, object_root: [u8; 32]) -> EcdsaSignature { + match self { + EcdsaSigner::Local(sk) => { + let domain = chain.builder_domain(); + let signing_root = compute_signing_root(object_root, domain); + k256::ecdsa::signature::Signer::::sign(sk, &signing_root) + .into() + } + } + } + + pub async fn sign_msg(&self, chain: Chain, msg: &impl TreeHash) -> EcdsaSignature { + self.sign(chain, msg.tree_hash_root().0).await + } +} + +pub fn verify_ecdsa_signature( + pubkey: &EcdsaPublicKey, + msg: &[u8], + signature: &EcdsaSignature, +) -> Result<(), k256::ecdsa::Error> { + use k256::ecdsa::signature::Verifier; + let ecdsa_pubkey = EcdsaPublicKeyInner::from_sec1_bytes(&pubkey.encoded)?; + let ecdsa_sig = + EcdsaSignatureInner::from_bytes(GenericArray::::from_slice(signature.as_ref()))?; + ecdsa_pubkey.verify(msg, &ecdsa_sig) +} diff --git a/crates/common/src/signer/schemes/mod.rs b/crates/common/src/signer/schemes/mod.rs new file mode 100644 index 00000000..91cccfdc --- /dev/null +++ b/crates/common/src/signer/schemes/mod.rs @@ -0,0 +1,2 @@ +pub mod bls; +pub mod ecdsa; diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 8912571c..ffa6d628 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -22,7 +22,10 @@ tokio.workspace = true tracing.workspace = true # crypto +blst.workspace = true tree_hash.workspace = true +tree_hash_derive.workspace = true +k256.workspace = true # misc thiserror.workspace = true @@ -30,3 +33,4 @@ eyre.workspace = true uuid.workspace = true bimap.workspace = true lazy_static.workspace = true +derive_more.workspace = true diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index ddf023af..8bdbd0ef 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -1,4 +1,4 @@ -use alloy::rpc::types::beacon::BlsPublicKey; +use alloy::hex; use axum::{ http::StatusCode, response::{IntoResponse, Response}, @@ -10,11 +10,11 @@ pub enum SignerModuleError { #[error("unauthorized")] Unauthorized, - #[error("unknown consensus signer: {0}")] - UnknownConsensusSigner(BlsPublicKey), + #[error("unknown consensus signer: 0x{}", hex::encode(.0))] + UnknownConsensusSigner(Vec), - #[error("unknown proxy signer: {0}")] - UnknownProxySigner(BlsPublicKey), + #[error("unknown proxy signer: 0x{}", hex::encode(.0))] + UnknownProxySigner(Vec), } impl IntoResponse for SignerModuleError { diff --git a/crates/signer/src/manager.rs b/crates/signer/src/manager.rs index 09ce3696..5353edfc 100644 --- a/crates/signer/src/manager.rs +++ b/crates/signer/src/manager.rs @@ -1,11 +1,21 @@ use std::collections::HashMap; -use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; +use alloy::rpc::types::beacon::BlsSignature; use cb_common::{ - commit::request::{ProxyDelegation, SignedProxyDelegation}, - signer::Signer, + commit::request::{ + ProxyDelegationBls, ProxyDelegationEcdsa, SignedProxyDelegationBls, + SignedProxyDelegationEcdsa, + }, + signer::{ + schemes::{ + bls::BlsPublicKey, + ecdsa::{EcdsaPublicKey, EcdsaSignature}, + }, + BlsSigner, ConsensusSigner, EcdsaSigner, + }, types::{Chain, ModuleId}, }; +use derive_more::derive::Deref; use tree_hash::TreeHash; use crate::error::SignerModuleError; @@ -16,60 +26,98 @@ use crate::error::SignerModuleError; // with its consensus pubkey When a new commit module starts, pass the // ProxyDelegation msg and then sign all future commit messages with the proxy // key for slashing the faulty message + proxy delegation can be used -// Signed using builder domain +#[derive(Clone, Deref)] +pub struct BlsProxySigner { + #[deref] + signer: BlsSigner, + delegation: SignedProxyDelegationBls, +} + +#[derive(Clone, Deref)] +pub struct EcdsaProxySigner { + #[deref] + signer: EcdsaSigner, + delegation: SignedProxyDelegationEcdsa, +} -#[derive(Clone)] -pub struct ProxySigner { - signer: Signer, - delegation: SignedProxyDelegation, +#[derive(Default)] +struct ProxySigners { + bls_signers: HashMap, + ecdsa_signers: HashMap, } pub struct SigningManager { chain: Chain, - consensus_signers: HashMap, - proxy_signers: HashMap, + consensus_signers: HashMap, + proxy_signers: ProxySigners, /// Map of module ids to their associated proxy pubkeys. /// Used to retrieve the corresponding proxy signer from the signing /// manager. - proxy_pubkeys: HashMap>, + proxy_pubkeys_bls: HashMap>, + proxy_pubkeys_ecdsa: HashMap>, } impl SigningManager { pub fn new(chain: Chain) -> Self { Self { chain, - consensus_signers: HashMap::new(), - proxy_signers: HashMap::new(), - proxy_pubkeys: HashMap::new(), + consensus_signers: Default::default(), + proxy_signers: Default::default(), + proxy_pubkeys_bls: Default::default(), + proxy_pubkeys_ecdsa: Default::default(), } } - pub fn add_consensus_signer(&mut self, signer: Signer) { + pub fn add_consensus_signer(&mut self, signer: ConsensusSigner) { self.consensus_signers.insert(signer.pubkey(), signer); } - pub fn add_proxy_signer(&mut self, proxy: ProxySigner) { - self.proxy_signers.insert(proxy.signer.pubkey(), proxy); + pub fn add_proxy_signer_bls(&mut self, proxy: BlsProxySigner, module_id: ModuleId) { + let proxy_pubkey = proxy.pubkey(); + self.proxy_signers.bls_signers.insert(proxy.pubkey(), proxy); + self.proxy_pubkeys_bls.entry(module_id).or_default().push(proxy_pubkey) + } + + pub fn add_proxy_signer_ecdsa(&mut self, proxy: EcdsaProxySigner, module_id: ModuleId) { + let proxy_pubkey = proxy.pubkey(); + self.proxy_signers.ecdsa_signers.insert(proxy.pubkey(), proxy); + self.proxy_pubkeys_ecdsa.entry(module_id).or_default().push(proxy_pubkey) + } + + pub async fn create_proxy_bls( + &mut self, + module_id: ModuleId, + delegator: BlsPublicKey, + ) -> Result { + let signer = BlsSigner::new_random(); + let proxy_pubkey = signer.pubkey(); + + let message = ProxyDelegationBls { delegator, proxy: proxy_pubkey }; + let signature = self.sign_consensus(&delegator, &message.tree_hash_root().0).await?; + let delegation = SignedProxyDelegationBls { signature, message }; + let proxy_signer = BlsProxySigner { signer, delegation }; + + self.add_proxy_signer_bls(proxy_signer, module_id); + + Ok(delegation) } - pub async fn create_proxy( + pub async fn create_proxy_ecdsa( &mut self, module_id: ModuleId, delegator: BlsPublicKey, - ) -> Result { - let signer = Signer::new_random(); + ) -> Result { + let signer = EcdsaSigner::new_random(); let proxy_pubkey = signer.pubkey(); - let message = ProxyDelegation { delegator, proxy: proxy_pubkey }; + let message = ProxyDelegationEcdsa { delegator, proxy: proxy_pubkey }; let signature = self.sign_consensus(&delegator, &message.tree_hash_root().0).await?; - let signed_delegation: SignedProxyDelegation = SignedProxyDelegation { signature, message }; - let proxy_signer = ProxySigner { signer, delegation: signed_delegation }; + let delegation = SignedProxyDelegationEcdsa { signature, message }; + let proxy_signer = EcdsaProxySigner { signer, delegation }; - // Add the new proxy key to the manager's internal state - self.add_proxy_signer(proxy_signer); - self.proxy_pubkeys.entry(module_id).or_default().push(proxy_pubkey); + self.add_proxy_signer_ecdsa(proxy_signer, module_id); - Ok(signed_delegation) + Ok(delegation) } // TODO: double check what we can actually sign here with different providers eg @@ -77,26 +125,42 @@ impl SigningManager { pub async fn sign_consensus( &self, pubkey: &BlsPublicKey, - msg: &[u8; 32], + object_root: &[u8; 32], ) -> Result { let signer = self .consensus_signers .get(pubkey) - .ok_or(SignerModuleError::UnknownConsensusSigner(*pubkey))?; - let signature = signer.sign(self.chain, msg).await; + .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + let signature = signer.sign(self.chain, *object_root).await; Ok(signature) } - pub async fn sign_proxy( + pub async fn sign_proxy_bls( &self, pubkey: &BlsPublicKey, - msg: &[u8; 32], + object_root: &[u8; 32], ) -> Result { - let proxy = - self.proxy_signers.get(pubkey).ok_or(SignerModuleError::UnknownProxySigner(*pubkey))?; - let signature = proxy.signer.sign(self.chain, msg).await; + let bls_proxy = self + .proxy_signers + .bls_signers + .get(pubkey) + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; + let signature = bls_proxy.sign(self.chain, *object_root).await; + Ok(signature) + } + pub async fn sign_proxy_ecdsa( + &self, + pubkey: &EcdsaPublicKey, + object_root: &[u8; 32], + ) -> Result { + let ecdsa_proxy = self + .proxy_signers + .ecdsa_signers + .get(pubkey) + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; + let signature = ecdsa_proxy.sign(self.chain, *object_root).await; Ok(signature) } @@ -104,37 +168,52 @@ impl SigningManager { self.consensus_signers.keys().cloned().collect() } - pub fn proxy_pubkeys(&self) -> &HashMap> { - &self.proxy_pubkeys + pub fn proxy_pubkeys_bls(&self) -> &HashMap> { + &self.proxy_pubkeys_bls } - pub fn delegations(&self) -> Vec { - self.proxy_signers.values().map(|s| s.delegation).collect() + pub fn proxy_pubkeys_ecdsa(&self) -> &HashMap> { + &self.proxy_pubkeys_ecdsa } pub fn has_consensus(&self, pubkey: &BlsPublicKey) -> bool { self.consensus_signers.contains_key(pubkey) } - pub fn has_proxy(&self, pubkey: &BlsPublicKey) -> bool { - self.proxy_signers.contains_key(pubkey) + pub fn has_proxy_ecdsa(&self, ecdsa_pk: &EcdsaPublicKey) -> bool { + self.proxy_signers.ecdsa_signers.contains_key(ecdsa_pk) + } + + pub fn has_proxy_bls(&self, bls_pk: &BlsPublicKey) -> bool { + self.proxy_signers.bls_signers.contains_key(bls_pk) } - pub fn get_delegation( + pub fn get_delegation_bls( &self, - proxy_pubkey: &BlsPublicKey, - ) -> Result { - let signer = self - .proxy_signers - .get(proxy_pubkey) - .ok_or(SignerModuleError::UnknownProxySigner(*proxy_pubkey))?; - Ok(signer.delegation) + pubkey: &BlsPublicKey, + ) -> Result { + self.proxy_signers + .bls_signers + .get(pubkey) + .map(|x| x.delegation) + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.as_ref().to_vec())) + } + + pub fn get_delegation_ecdsa( + &self, + pubkey: &EcdsaPublicKey, + ) -> Result { + self.proxy_signers + .ecdsa_signers + .get(pubkey) + .map(|x| x.delegation) + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.as_ref().to_vec())) } } #[cfg(test)] mod tests { - use cb_common::signature::verify_signed_builder_message; + use cb_common::signature::compute_signing_root; use lazy_static::lazy_static; use tree_hash::Hash256; @@ -148,7 +227,7 @@ mod tests { fn init_signing_manager() -> (SigningManager, BlsPublicKey) { let mut signing_manager = SigningManager::new(*CHAIN); - let consensus_signer = Signer::new_random(); + let consensus_signer = ConsensusSigner::new_random(); let consensus_pk = consensus_signer.pubkey(); signing_manager.add_consensus_signer(consensus_signer.clone()); @@ -156,64 +235,153 @@ mod tests { (signing_manager, consensus_pk) } - #[tokio::test] - async fn test_proxy_key_is_valid_proxy_for_consensus_key() { - let (mut signing_manager, consensus_pk) = init_signing_manager(); + mod test_proxy_bls { + use cb_common::signer::schemes::bls::verify_bls_signature; - let signed_delegation = - signing_manager.create_proxy(MODULE_ID.clone(), consensus_pk.clone()).await.unwrap(); + use super::*; - let validation_result = signed_delegation.validate(*CHAIN); + #[tokio::test] + async fn test_proxy_key_is_valid_proxy_for_consensus_key() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); - assert!( - validation_result.is_ok(), - "Proxy delegation signature must be valid for consensus key." - ); + let signed_delegation = signing_manager + .create_proxy_bls(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); - assert!( - signing_manager.has_proxy(&signed_delegation.message.proxy), - "Newly generated proxy key must be present in the signing manager's registry." - ); - } + let validation_result = signed_delegation.validate(*CHAIN); + + assert!( + validation_result.is_ok(), + "Proxy delegation signature must be valid for consensus key." + ); + + assert!( + signing_manager.has_proxy_bls(&signed_delegation.message.proxy), + "Newly generated proxy key must be present in the signing manager's registry." + ); + } + + #[tokio::test] + async fn test_tampered_proxy_key_is_invalid() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); - #[tokio::test] - async fn test_tampered_proxy_key_is_invalid() { - let (mut signing_manager, consensus_pk) = init_signing_manager(); + let mut signed_delegation = signing_manager + .create_proxy_bls(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); - let mut signed_delegation = - signing_manager.create_proxy(MODULE_ID.clone(), consensus_pk.clone()).await.unwrap(); + let m = &mut signed_delegation.signature.0[0]; + (*m, _) = m.overflowing_add(1); - let m = &mut signed_delegation.signature.0[0]; - (*m, _) = m.overflowing_add(1); + let validation_result = signed_delegation.validate(*CHAIN); + + assert!(validation_result.is_err(), "Tampered proxy key must be invalid."); + } - let validation_result = signed_delegation.validate(*CHAIN); + #[tokio::test] + async fn test_proxy_key_signs_message() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); - assert!(validation_result.is_err(), "Tampered proxy key must be invalid."); + let signed_delegation = signing_manager + .create_proxy_bls(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); + let proxy_pk = signed_delegation.message.proxy; + + let data_root = Hash256::random(); + let data_root_bytes = data_root.as_fixed_bytes(); + + let sig = signing_manager + .sign_proxy_bls(&proxy_pk.try_into().unwrap(), data_root_bytes) + .await + .unwrap(); + + // Verify signature + let domain = CHAIN.builder_domain(); + let signing_root = compute_signing_root(data_root_bytes.tree_hash_root().0, domain); + + let validation_result = verify_bls_signature(&proxy_pk, &signing_root, &sig); + + assert!( + validation_result.is_ok(), + "Proxy keypair must produce valid signatures of messages." + ) + } } - #[tokio::test] - async fn test_proxy_key_signs_message() { - let (mut signing_manager, consensus_pk) = init_signing_manager(); + mod test_proxy_ecdsa { + use cb_common::signer::schemes::ecdsa::verify_ecdsa_signature; + + use super::*; - let signed_delegation = - signing_manager.create_proxy(MODULE_ID.clone(), consensus_pk.clone()).await.unwrap(); - let proxy_pk = signed_delegation.message.proxy; + #[tokio::test] + async fn test_proxy_key_is_valid_proxy_for_consensus_key() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); - let data_root = Hash256::random(); - let data_root_bytes = data_root.as_fixed_bytes(); + let signed_delegation = signing_manager + .create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); - let sig = signing_manager.sign_proxy(&proxy_pk, data_root_bytes).await.unwrap(); + let validation_result = signed_delegation.validate(*CHAIN); - let validation_result = verify_signed_builder_message( - *CHAIN, - &signed_delegation.message.proxy, - &data_root_bytes, - &sig, - ); + assert!( + validation_result.is_ok(), + "Proxy delegation signature must be valid for consensus key." + ); - assert!( - validation_result.is_ok(), - "Proxy keypair must produce valid signatures of messages." - ) + assert!( + signing_manager.has_proxy_ecdsa(&signed_delegation.message.proxy), + "Newly generated proxy key must be present in the signing manager's registry." + ); + } + + #[tokio::test] + async fn test_tampered_proxy_key_is_invalid() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); + + let mut signed_delegation = signing_manager + .create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); + + let m = &mut signed_delegation.signature.0[0]; + (*m, _) = m.overflowing_add(1); + + let validation_result = signed_delegation.validate(*CHAIN); + + assert!(validation_result.is_err(), "Tampered proxy key must be invalid."); + } + + #[tokio::test] + async fn test_proxy_key_signs_message() { + let (mut signing_manager, consensus_pk) = init_signing_manager(); + + let signed_delegation = signing_manager + .create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk.clone()) + .await + .unwrap(); + let proxy_pk = signed_delegation.message.proxy; + + let data_root = Hash256::random(); + let data_root_bytes = data_root.as_fixed_bytes(); + + let sig = signing_manager + .sign_proxy_ecdsa(&proxy_pk.try_into().unwrap(), data_root_bytes) + .await + .unwrap(); + + // Verify signature + let domain = CHAIN.builder_domain(); + let signing_root = compute_signing_root(data_root_bytes.tree_hash_root().0, domain); + + let validation_result = verify_ecdsa_signature(&proxy_pk, &signing_root, &sig); + + assert!( + validation_result.is_ok(), + "Proxy keypair must produce valid signatures of messages." + ) + } } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e18b90c5..c4032aa7 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -12,9 +12,11 @@ use axum_extra::TypedHeader; use bimap::BiHashMap; use cb_common::{ commit::{ - client::GetPubkeysResponse, constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH}, - request::{GenerateProxyRequest, SignRequest}, + request::{ + EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, + SignProxyRequest, SignRequest, + }, }, config::StartSignerConfig, types::{Jwt, ModuleId}, @@ -108,9 +110,12 @@ async fn handle_get_pubkeys( let signing_manager = state.manager.read().await; let consensus = signing_manager.consensus_pubkeys(); - let proxy = signing_manager.proxy_pubkeys().get(&module_id).cloned().unwrap_or_default(); + let proxy_bls = + signing_manager.proxy_pubkeys_bls().get(&module_id).cloned().unwrap_or_default(); + let proxy_ecdsa = + signing_manager.proxy_pubkeys_ecdsa().get(&module_id).cloned().unwrap_or_default(); - let res = GetPubkeysResponse { consensus, proxy }; + let res = GetPubkeysResponse { consensus, proxy_bls, proxy_ecdsa }; Ok((StatusCode::OK, Json(res)).into_response()) } @@ -127,13 +132,24 @@ async fn handle_request_signature( let signing_manager = state.manager.read().await; - let sig = if request.is_proxy { - signing_manager.sign_proxy(&request.pubkey, &request.object_root).await - } else { - signing_manager.sign_consensus(&request.pubkey, &request.object_root).await + let signature_response = match request { + SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager + .sign_consensus(&pubkey, &object_root) + .await + .map(|sig| Json(sig).into_response()), + SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => signing_manager + .sign_proxy_bls(&bls_pk, &object_root) + .await + .map(|sig| Json(sig).into_response()), + SignRequest::ProxyEcdsa(SignProxyRequest { pubkey: ecdsa_pk, object_root }) => { + signing_manager + .sign_proxy_ecdsa(&ecdsa_pk, &object_root) + .await + .map(|sig| Json(sig).into_response()) + } }?; - Ok((StatusCode::OK, Json(sig)).into_response()) + Ok(signature_response) } async fn handle_generate_proxy( @@ -147,7 +163,18 @@ async fn handle_generate_proxy( let mut signing_manager = state.manager.write().await; - let proxy_delegation = signing_manager.create_proxy(module_id, request.pubkey).await?; + let response = match request.scheme { + EncryptionScheme::Bls => { + let proxy_delegation = + signing_manager.create_proxy_bls(module_id, request.consensus_pubkey).await?; + Json(proxy_delegation).into_response() + } + EncryptionScheme::Ecdsa => { + let proxy_delegation = + signing_manager.create_proxy_ecdsa(module_id, request.consensus_pubkey).await?; + Json(proxy_delegation).into_response() + } + }; - Ok((StatusCode::OK, Json(proxy_delegation)).into_response()) + Ok(response) } diff --git a/docs/docs/developing/commit-module.md b/docs/docs/developing/commit-module.md index cfd388c9..f1ad147d 100644 --- a/docs/docs/developing/commit-module.md +++ b/docs/docs/developing/commit-module.md @@ -46,7 +46,7 @@ The loaded `config` also has a few other useful fields: ## Requesting signatures -At its core the Signer Module simply provides a signature on a 32-byte data digest. The signatures are currently provided with either the validator keys (BLS) or a proxy key (also BLS) for a given validator key, both on the [builder domain](https://github.com/Commit-Boost/commit-boost-client/blob/main/crates/common/src/signature.rs#L88-L96). Eventually we plan to support [alternative](https://github.com/Commit-Boost/commit-boost-client/issues/20) signing schemes, too. +At its core the Signer Module simply provides a signature on a 32-byte data digest. The signatures are currently provided with either the validator keys (BLS) or a proxy key (BLS or ECDSA) for a given validator key, both on the [builder domain](https://github.com/Commit-Boost/commit-boost-client/blob/main/crates/common/src/signature.rs#L88-L96). In the example we use `TreeHash`, already used in the CL, to create the digest from a custom struct: ```rust @@ -69,26 +69,37 @@ Then, we can request a signature either with a consensus key or with a proxy key Requesting a signature is as simple as: ```rust let datagram = Datagram { data: 1 }; -let request = SignRequest::builder(pubkey).with_msg(&datagram); -let signature = config.signer_client.request_signature(&request).await.unwrap(); +let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); +let signature = config.signer_client.request_consensus_signature(&request).await.unwrap(); ``` Where `pubkey` is the validator (consensus) public key for which the signature is requested. ### With a proxy key -You'll have to first request a proxy key be generated for a given consensus key: +You'll have to first request a proxy key be generated for a given consensus key. +We support two signature schemes for proxies: BLS or ECDSA. + +To request a proxy: ```rust -let proxy_delegation = self.config.signer_client.generate_proxy_key(pubkey).await?; +// BLS proxy +let proxy_delegation = self.config.signer_client.generate_proxy_key_bls(pubkey).await?; +let proxy_pubkey = proxy_delegation.message.proxy; + +// or ECDSA proxy +let proxy_delegation = self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; let proxy_pubkey = proxy_delegation.message.proxy; ``` Where `pubkey` is the validator (consensus) public key for which a proxy is to be generated. -Then, the only difference from the direct approach is explicitly saying that the signature request uses a proxy key: +Then you can use the generated proxy key to request a signature: ```rust let datagram = Datagram { data: 1 }; -let request = SignRequest::builder(proxy_pubkey).is_proxy().with_msg(&datagram); -let signature = config.signer_client.request_signature(&request).await.unwrap(); +let request = SignProxyRequest::builder(proxy_pubkey).with_msg(&datagram); +// if `proxy_pubkey` is a BLS proxy +let signature = config.signer_client.request_proxy_signature_bls(&request).await.unwrap(); +// or for ECDSA proxy +let signature = config.signer_client.request_proxy_signature_ecdsa(&request).await.unwrap(); ``` ## Metrics diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 87ead506..09abcf2c 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; use commit_boost::prelude::*; use eyre::{OptionExt, Result}; use lazy_static::lazy_static; @@ -41,18 +40,29 @@ impl DaCommitService { // the config has the signer_client already setup, we can use it to interact // with the Signer API let pubkeys = self.config.signer_client.get_pubkeys().await?; - info!(consensus = pubkeys.consensus.len(), proxy = pubkeys.proxy.len(), "Received pubkeys"); - - let pubkey = pubkeys.consensus.first().ok_or_eyre("no key available")?; + info!( + consensus = pubkeys.consensus.len(), + proxy_bls = pubkeys.proxy_bls.len(), + proxy_ecdsa = pubkeys.proxy_ecdsa.len(), + "Received pubkeys" + ); + + let pubkey = *pubkeys.consensus.first().ok_or_eyre("no key available")?; info!("Registered validator {pubkey}"); - let proxy_delegation = self.config.signer_client.generate_proxy_key(*pubkey).await?; - info!("Obtained a proxy delegation {proxy_delegation:#?}"); + let proxy_delegation_bls = self.config.signer_client.generate_proxy_key_bls(pubkey).await?; + info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); + let proxy_bls = proxy_delegation_bls.message.proxy; + + let proxy_delegation_ecdsa = + self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; + info!("Obtained an ECDSA proxy delegation:\n{proxy_delegation_ecdsa}"); + let proxy_ecdsa = proxy_delegation_ecdsa.message.proxy; let mut data = 0; loop { - self.send_request(data, *pubkey, proxy_delegation).await?; + self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; data += 1; } @@ -62,24 +72,30 @@ impl DaCommitService { &self, data: u64, pubkey: BlsPublicKey, - proxy_delegation: SignedProxyDelegation, + proxy_bls: BlsPublicKey, + proxy_ecdsa: EcdsaPublicKey, ) -> Result<()> { let datagram = Datagram { data }; - let request = SignRequest::builder(pubkey).with_msg(&datagram); - let signature = self.config.signer_client.request_signature(&request); + let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); + let signature = self.config.signer_client.request_consensus_signature(request); + + let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); + let proxy_signature_bls = + self.config.signer_client.request_proxy_signature_bls(proxy_request_bls); - let proxy_request = - SignRequest::builder(proxy_delegation.message.proxy).is_proxy().with_msg(&datagram); - let proxy_signature = self.config.signer_client.request_signature(&proxy_request); + let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); + let proxy_signature_ecdsa = + self.config.signer_client.request_proxy_signature_ecdsa(proxy_request_ecdsa); - let (signature, proxy_signature) = { - let res = tokio::join!(signature, proxy_signature); - (res.0?, res.1?) + let (signature, proxy_signature_bls, proxy_signature_ecdsa) = { + let res = tokio::join!(signature, proxy_signature_bls, proxy_signature_ecdsa); + (res.0?, res.1?, res.2?) }; - info!("Proposer commitment (consensus): {}", pretty_print_sig(signature)); - info!("Proposer commitment (proxy): {}", pretty_print_sig(proxy_signature)); + info!("Proposer commitment (consensus): {}", signature); + info!("Proposer commitment (proxy BLS): {}", proxy_signature_bls); + info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); SIG_RECEIVED_COUNTER.inc(); @@ -118,7 +134,3 @@ async fn main() -> Result<()> { } Ok(()) } - -fn pretty_print_sig(sig: BlsSignature) -> String { - format!("{}..", &sig.to_string()[..16]) -} diff --git a/tests/src/mock_relay.rs b/tests/src/mock_relay.rs index 25a489fa..36a5802b 100644 --- a/tests/src/mock_relay.rs +++ b/tests/src/mock_relay.rs @@ -16,7 +16,7 @@ use cb_common::{ GetHeaderParams, GetHeaderReponse, SubmitBlindedBlockResponse, BUILDER_API_PATH, GET_HEADER_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH, }, - signer::Signer, + signer::ConsensusSigner, types::Chain, }; use tracing::debug; @@ -25,7 +25,7 @@ use tree_hash::TreeHash; pub struct MockRelayState { pub chain: Chain, pub get_header_delay_ms: u64, - pub signer: Signer, + pub signer: ConsensusSigner, received_get_header: Arc, received_get_status: Arc, received_register_validator: Arc, @@ -48,7 +48,7 @@ impl MockRelayState { } impl MockRelayState { - pub fn new(chain: Chain, signer: Signer, get_header_delay_ms: u64) -> Self { + pub fn new(chain: Chain, signer: ConsensusSigner, get_header_delay_ms: u64) -> Self { Self { chain, signer, @@ -82,9 +82,9 @@ async fn handle_get_header( response.data.message.header.parent_hash = parent_hash; response.data.message.header.block_hash.0[0] = 1; response.data.message.set_value(U256::from(10)); - response.data.message.pubkey = state.signer.pubkey(); - response.data.signature = - state.signer.sign(state.chain, &response.data.message.tree_hash_root().0).await; + response.data.message.pubkey = state.signer.pubkey().into(); + let object_root = response.data.message.tree_hash_root().0; + response.data.signature = state.signer.sign(state.chain, object_root).await; (StatusCode::OK, axum::Json(response)).into_response() } diff --git a/tests/tests/pbs_integration.rs b/tests/tests/pbs_integration.rs index eb3cff56..fd0d7eea 100644 --- a/tests/tests/pbs_integration.rs +++ b/tests/tests/pbs_integration.rs @@ -4,7 +4,7 @@ use alloy::primitives::U256; use cb_common::{ config::{PbsConfig, PbsModuleConfig}, pbs::RelayClient, - signer::Signer, + signer::ConsensusSigner, types::Chain, }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; @@ -55,12 +55,12 @@ fn to_pbs_config(chain: Chain, pbs_config: PbsConfig, relays: Vec) #[tokio::test] async fn test_get_header() -> Result<()> { setup_test_env(); - let signer = Signer::new_random(); + let signer = ConsensusSigner::new_random(); let chain = Chain::Holesky; let port = 3000; - let mock_relay = generate_mock_relay(port + 1, signer.pubkey())?; + let mock_relay = generate_mock_relay(port + 1, *signer.pubkey())?; let mock_state = Arc::new(MockRelayState::new(chain, signer, 0)); tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); @@ -83,14 +83,14 @@ async fn test_get_header() -> Result<()> { #[tokio::test] async fn test_get_status() -> Result<()> { setup_test_env(); - let signer = Signer::new_random(); + let signer = ConsensusSigner::new_random(); let chain = Chain::Holesky; let port = 3100; let relays = vec![ - generate_mock_relay(port + 1, signer.pubkey())?, - generate_mock_relay(port + 2, signer.pubkey())?, + generate_mock_relay(port + 1, *signer.pubkey())?, + generate_mock_relay(port + 2, *signer.pubkey())?, ]; let mock_state = Arc::new(MockRelayState::new(chain, signer, 0)); tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); @@ -115,12 +115,12 @@ async fn test_get_status() -> Result<()> { #[tokio::test] async fn test_register_validators() -> Result<()> { setup_test_env(); - let signer = Signer::new_random(); + let signer = ConsensusSigner::new_random(); let chain = Chain::Holesky; let port = 3300; - let relays = vec![generate_mock_relay(port + 1, signer.pubkey())?]; + let relays = vec![generate_mock_relay(port + 1, *signer.pubkey())?]; let mock_state = Arc::new(MockRelayState::new(chain, signer, 0)); tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); @@ -143,12 +143,12 @@ async fn test_register_validators() -> Result<()> { #[tokio::test] async fn test_submit_block() -> Result<()> { setup_test_env(); - let signer = Signer::new_random(); + let signer = ConsensusSigner::new_random(); let chain = Chain::Holesky; let port = 3400; - let relays = vec![generate_mock_relay(port + 1, signer.pubkey())?]; + let relays = vec![generate_mock_relay(port + 1, *signer.pubkey())?]; let mock_state = Arc::new(MockRelayState::new(chain, signer, 0)); tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1));