Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ethereum_serde_utils = "0.5.2"
ethereum-types = "0.14.1"

# networking
axum = "0.7.5"
axum = { version = "0.7.5", features = ["macros"] }
axum-extra = { version = "0.9.3", features = ["typed-header"] }
reqwest = { version = "0.12.4", features = ["json"] }
headers = "0.4.0"
Expand Down
138 changes: 138 additions & 0 deletions api/signer-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
openapi: "3.0.2"
info:
title: Signer API
version: "0.1.0"
description: API that allows commit modules to request generic signatures from validators
tags:
- name: Signer
paths:
/signer/v1/get_pubkeys:
get:
summary: Get a list of public keys for which signatures may be requested
tags:
- Signer
responses:
"200":
description: A list of Bls pubkeys
content:
application/json:
schema:
type: object
properties:
consensus:
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
type: array
items:
type: string
format: hex
pattern: "^0x[a-fA-F0-9]{96}$"
example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989"

"500":
description: Internal error
content:
application/json:
schema:
type: object
required:
- code
- message
properties:
code:
type: number
example: 500
message:
type: string
example: "Internal error"

/signer/v1/request_signature:
post:
summary: Send a signature request
tags:
- Signer
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
description: The module ID
type: string
example: "MY_MODULE_ID"
pubkey:
description: BLS public key of validator
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
object_root:
description: The root of the object to be signed
type: string
format: hex
pattern: "^0x[a-fA-F0-9]{64}$"
example: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9"
responses:
"200":
description: Successs
content:
application/json:
schema:
type: string
description: The validator signature
format: hex
pattern: "^0x[a-fA-F0-9]{192}$"
example: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989"
"404":
description: Unknown value (pubkey, module id)
content:
application/json:
schema:
type: object
required:
- code
- message
properties:
code:
type: number
example: 404
message:
type: string
example: "Unknown pubkey"
"500":
description: Internal error
content:
application/json:
schema:
type: object
required:
- code
- message
properties:
code:
type: number
example: 500
message:
type: string
example: "Internal error"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
31 changes: 23 additions & 8 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::process::Stdio;
use std::{collections::HashMap, process::Stdio};

use cb_common::{
config::{CommitBoostConfig, CONFIG_PATH_ENV, MODULE_ID_ENV},
Expand Down Expand Up @@ -68,21 +68,31 @@ impl Args {
match self.cmd {
Command::Start { config: config_path } => {
let config = CommitBoostConfig::from_file(&config_path);
let signer_config = config.signer.expect("missing signer config with modules");

// start signing server
// TODO: generate jwt for each module id
let pbs_jwt = "MY_PBS_TOKEN";
let jwts = HashMap::from([("PBS_DEFAULT".into(), pbs_jwt.into())]);
let signer_address = signer_config.address;

// Initialize Docker client
let docker = bollard::Docker::connect_with_local_defaults().expect("Failed to connect to Docker");
let docker = bollard::Docker::connect_with_local_defaults()
.expect("Failed to connect to Docker");

if let Some(modules) = config.modules {
let signer_config = config.signer.expect("missing signer config with modules");
// start signing server
tokio::spawn(SigningService::run(config.chain, signer_config));
tokio::spawn(SigningService::run(config.chain, signer_config, jwts));

for module in modules {
let config = bollard::container::Config {
image: Some(module.docker_image.clone()),
host_config: Some(bollard::secret::HostConfig {
binds: {
let full_config_path = std::fs::canonicalize(&config_path).unwrap().to_string_lossy().to_string();
let full_config_path = std::fs::canonicalize(&config_path)
.unwrap()
.to_string_lossy()
.to_string();
Some(vec![format!("{}:{}", full_config_path, "/config.toml")])
},
network_mode: Some(String::from("host")), // Use the host network
Expand All @@ -95,11 +105,15 @@ impl Args {
..Default::default()
};

let container = docker.create_container::<&str, String>(None, config).await?;
let container =
docker.create_container::<&str, String>(None, config).await?;
let container_id = container.id;
docker.start_container::<String>(&container_id, None).await?;

println!("Started container: {} from image {}", container_id, module.docker_image);
println!(
"Started container: {} from image {}",
container_id, module.docker_image
);
}
}

Expand All @@ -116,7 +130,8 @@ impl Args {
eprintln!("Process failed with status: {}", cmd.status);
}
} else {
let state = BuilderState::<()>::new(config.chain, config.pbs);
let state =
BuilderState::<()>::new(config.chain, config.pbs, signer_address, pbs_jwt);
PbsService::run::<(), DefaultBuilderApi>(state).await;
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ blst.workspace = true
tree_hash.workspace = true
tree_hash_derive.workspace = true
rand.workspace = true

reqwest.workspace = true
thiserror.workspace = true
85 changes: 85 additions & 0 deletions crates/common/src/commit/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::{net::SocketAddr, sync::Arc};

use alloy_rpc_types_beacon::{BlsPublicKey, BlsSignature};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use serde::{Deserialize, Serialize};

use super::{
constants::{GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH},
error::SignerClientError,
request::SignRequest,
};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetPubkeysResponse {
pub consensus: Vec<BlsPublicKey>,
pub proxy: Vec<BlsPublicKey>,
}

/// Client used by commit modules to request signatures via the Signer API
#[derive(Debug, Clone)]
pub struct SignerClient {
/// Url endpoint of the Signer Module
url: Arc<str>,
client: reqwest::Client,
}

impl SignerClient {
/// Create a new SignerClient
pub fn new(signer_address: SocketAddr, jwt: &str) -> Result<Self, SignerClientError> {
let url = format!("http://{}", signer_address);
let mut headers = HeaderMap::new();

let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt))?;
auth_value.set_sensitive(true);
headers.insert(AUTHORIZATION, auth_value);

let client = reqwest::ClientBuilder::new().default_headers(headers).build()?;

Ok(Self { url: url.into(), client })
}

/// Request a list of validator pubkeys for which signatures can be requested.
/// TODO: add more docs on how proxy keys work
pub async fn get_pubkeys(&self) -> Result<GetPubkeysResponse, SignerClientError> {
let url = format!("{}{}", self.url, GET_PUBKEYS_PATH);
let res = self.client.get(&url).send().await?;

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

if !status.is_success() {
return Err(SignerClientError::FailedRequest {
status: status.as_u16(),
error_msg: String::from_utf8_lossy(&response_bytes).into_owned(),
});
}

let parsed_response: GetPubkeysResponse = serde_json::from_slice(&response_bytes)?;

Ok(parsed_response)
}

/// Send a signature request
pub async fn request_signature(
&self,
request: &SignRequest,
) -> Result<BlsSignature, SignerClientError> {
let url = format!("{}{}", self.url, REQUEST_SIGNATURE_PATH);
let res = self.client.post(&url).json(&request).send().await?;

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

if !status.is_success() {
return Err(SignerClientError::FailedRequest {
status: status.as_u16(),
error_msg: String::from_utf8_lossy(&response_bytes).into_owned(),
});
}

let signature: BlsSignature = serde_json::from_slice(&response_bytes)?;

Ok(signature)
}
}
2 changes: 2 additions & 0 deletions crates/common/src/commit/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub const GET_PUBKEYS_PATH: &str = "/signer/v1/get_pubkeys";
pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature";
14 changes: 14 additions & 0 deletions crates/common/src/commit/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#[derive(Debug, thiserror::Error)]
pub enum SignerClientError {
#[error("reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),

#[error("invalid header value: {0}")]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),

#[error("failed request: status {status} msg {error_msg}")]
FailedRequest { status: u16, error_msg: String },

#[error("serde decode error: {0}")]
SerdeDecodeError(#[from] serde_json::Error),
}
4 changes: 4 additions & 0 deletions crates/common/src/commit/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod client;
pub mod constants;
pub mod error;
pub mod request;
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use alloy_rpc_types_beacon::{BlsPublicKey, BlsSignature};
use blst::BLST_ERROR;
use cb_common::{signature::verify_signed_builder_message, types::Chain};
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;

use crate::{signature::verify_signed_builder_message, types::Chain};

// TODO: might need to adapt the SignedProxyDelegation so that it goes through web3 signer
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, TreeHash)]
pub struct ProxyDelegation {
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct SignerConfig {
/// Where to start signing server
pub address: SocketAddr,

/// Which keys to load
pub loader: SignerLoader,
}

Expand Down
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod commit;
pub mod config;
pub mod constants;
pub mod pbs;
Expand Down
3 changes: 0 additions & 3 deletions crates/common/src/pbs/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,4 @@ pub const HEADER_KEY_SLOT_UUID: &str = "X-MEVBoost-SlotID";
pub const HEADER_KEY_VERSION: &str = "X-MEVBoost-Version"; // do we need to use this
pub const HEADER_START_TIME_UNIX_MS: &str = "X-MEVBoost-StartTimeUnixMS";

pub const COMMIT_BOOST_API: &str = "/commit-boost/v1";
pub const SIGN_REQUEST_PATH: &str = "/signer/sign";
pub const PUBKEYS_PATH: &str = "/signer/pubkeys";
pub const BUILDER_EVENTS_PATH: &str = "/events";
3 changes: 2 additions & 1 deletion crates/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ ethereum_ssz.workspace = true
ethereum_ssz_derive.workspace = true

axum.workspace = true

axum-extra.workspace = true
headers.workspace = true
# crypto
blst.workspace = true
tree_hash.workspace = true
Expand Down
Loading