Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(signer): add ERC2335 ProxyStore #193

Merged
merged 20 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
7 changes: 6 additions & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,17 @@ key_path = "./keys.example.json"
# For teku, it's the path to the directory where all `<pubkey>.txt` files are located.
# For lodestar, it's the path to the file containing the decryption password.
# secrets_path = ""
# Configuration for how the Signer module should store proxy delegations. Currently one type of store is supported:
# Configuration for how the Signer module should store proxy delegations. Supported types of store are:
# - File: store keys and delegations from a plain text file (unsafe, use only for testing purposes)
# - ERC2335: store keys and delegations safely using ERC-2335 style keystores. More details can be found in the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration#proxy-keys-store)
# OPTIONAL, if missing proxies are lost on restart
[signer.local.store]
# File: path to the keys file
proxy_dir = "./proxies"
# ERC2335: path to the keys directory
# keys_path = ""
ltitanb marked this conversation as resolved.
Show resolved Hide resolved
# ERC2335: path to the secrets directory
# secrets_path = ""
ltitanb marked this conversation as resolved.
Show resolved Hide resolved

# Commit-Boost can optionally run "modules" which extend the capabilities of the sidecar.
# Currently, two types of modules are supported:
Expand Down
49 changes: 42 additions & 7 deletions crates/cli/src/docker_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ use cb_common::{
CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV,
BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, JWTS_ENV, LOGS_DIR_DEFAULT,
LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV,
PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT,
SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV,
SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV,
PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT,
PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT,
SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT,
SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV,
SIGNER_URL_ENV,
},
signer::{ProxyStore, SignerLoader},
types::ModuleId,
utils::random_jwt,
};
use docker_compose_types::{
Compose, ComposeVolume, DependsOnOptions, EnvFile, Environment, Labels, LoggingParameters,
MapOrEmpty, NetworkSettings, Networks, Ports, Service, Services, SingleValue, TopLevelVolumes,
Volumes,
Compose, ComposeVolume, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck,
HealthcheckTest, Labels, LoggingParameters, MapOrEmpty, NetworkSettings, Networks, Ports,
Service, Services, SingleValue, TopLevelVolumes, Volumes,
};
use eyre::Result;
use indexmap::IndexMap;
Expand Down Expand Up @@ -151,6 +153,12 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
module_volumes.extend(chain_spec_volume.clone());
module_volumes.extend(get_log_volume(&cb_config.logs, &module.id));

// depends_on
let mut module_dependencies = IndexMap::new();
module_dependencies.insert("cb_signer".into(), DependsCondition {
condition: "service_healthy".into(),
});

Service {
container_name: Some(module_cid.clone()),
image: Some(module.docker_image),
Expand All @@ -160,7 +168,7 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
depends_on: if let Some(SignerConfig::Remote { .. }) = &cb_config.signer {
DependsOnOptions::Simple(vec![])
} else {
DependsOnOptions::Simple(vec!["cb_signer".to_owned()])
DependsOnOptions::Conditional(module_dependencies)
},
env_file,
..Service::default()
Expand Down Expand Up @@ -367,6 +375,23 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
let (k, v) = get_env_val(PROXY_DIR_ENV, PROXY_DIR_DEFAULT);
signer_envs.insert(k, v);
}
ProxyStore::ERC2335 { keys_path, secrets_path } => {
volumes.push(Volumes::Simple(format!(
"{}:{}:rw",
keys_path.display(),
PROXY_DIR_KEYS_DEFAULT
)));
let (k, v) = get_env_val(PROXY_DIR_KEYS_ENV, PROXY_DIR_KEYS_DEFAULT);
signer_envs.insert(k, v);

volumes.push(Volumes::Simple(format!(
"{}:{}:rw",
secrets_path.display(),
PROXY_DIR_SECRETS_DEFAULT
)));
let (k, v) = get_env_val(PROXY_DIR_SECRETS_ENV, PROXY_DIR_SECRETS_DEFAULT);
signer_envs.insert(k, v);
}
}
}

Expand All @@ -384,6 +409,16 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
networks: Networks::Simple(signer_networks),
volumes,
environment: Environment::KvPair(signer_envs),
healthcheck: Some(Healthcheck {
test: Some(HealthcheckTest::Single(
"curl -f http://localhost:20000/status".into(),
ltitanb marked this conversation as resolved.
Show resolved Hide resolved
)),
interval: Some("5s".into()),
timeout: Some("5s".into()),
retries: 5,
start_period: Some("0s".into()),
disable: false,
}),
..Service::default()
};

Expand Down
1 change: 1 addition & 0 deletions crates/common/src/commit/constants.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub const GET_PUBKEYS_PATH: &str = "/signer/v1/get_pubkeys";
pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature";
pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key";
pub const STATUS_PATH: &str = "/status";
26 changes: 25 additions & 1 deletion crates/common/src/commit/request.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::fmt::{self, Debug, Display, LowerHex};
use std::{
fmt::{self, Debug, Display, LowerHex},
str::FromStr,
};

use alloy::rpc::types::beacon::BlsSignature;
use derive_more::derive::From;
Expand Down Expand Up @@ -133,6 +136,27 @@ pub enum EncryptionScheme {
Ecdsa,
}

impl Display for EncryptionScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncryptionScheme::Bls => write!(f, "bls"),
EncryptionScheme::Ecdsa => write!(f, "ecdsa"),
}
}
}

impl FromStr for EncryptionScheme {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bls" => Ok(EncryptionScheme::Bls),
"ecdsa" => Ok(EncryptionScheme::Ecdsa),
_ => Err(format!("Unknown scheme: {s}")),
}
}
}

// TODO(David): This struct shouldn't be visible to module authors
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateProxyRequest {
Expand Down
8 changes: 7 additions & 1 deletion crates/common/src/config/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ pub const SIGNER_DIR_KEYS_DEFAULT: &str = "/keys";
/// Path to `secrets` folder
pub const SIGNER_DIR_SECRETS_ENV: &str = "CB_SIGNER_LOADER_SECRETS_DIR";
pub const SIGNER_DIR_SECRETS_DEFAULT: &str = "/secrets";
/// Path to store proxies
/// Path to store proxies with plaintext keys (testing only)
pub const PROXY_DIR_ENV: &str = "CB_PROXY_STORE_DIR";
pub const PROXY_DIR_DEFAULT: &str = "/proxies";
/// Path to store proxy keys
pub const PROXY_DIR_KEYS_ENV: &str = "CB_PROXY_KEYS_DIR";
pub const PROXY_DIR_KEYS_DEFAULT: &str = "/proxy_keys";
/// Path to store proxy secrets
pub const PROXY_DIR_SECRETS_ENV: &str = "CB_PROXY_SECRETS_DIR";
pub const PROXY_DIR_SECRETS_DEFAULT: &str = "/proxy_secrets";

///////////////////////// MODULES /////////////////////////

Expand Down
24 changes: 20 additions & 4 deletions crates/common/src/signer/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ use aes::{
Aes128,
};
use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey};
use eth2_keystore::Keystore;
use eth2_keystore::{json_keystore::JsonKeystore, Keystore};
use eyre::{eyre, Context, OptionExt};
use pbkdf2::{hmac, pbkdf2};
use serde::{de, Deserialize, Deserializer, Serialize};
use tracing::warn;
use unicode_normalization::UnicodeNormalization;

use super::{PrysmDecryptedKeystore, PrysmKeystore};
use super::{BlsSigner, EcdsaSigner, PrysmDecryptedKeystore, PrysmKeystore};
use crate::{
config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV},
signer::ConsensusSigner,
Expand Down Expand Up @@ -56,8 +56,10 @@ impl SignerLoader {

pub fn load_from_env(self) -> eyre::Result<Vec<ConsensusSigner>> {
Ok(match self {
SignerLoader::File { .. } => {
let path = load_env_var(SIGNER_KEYS_ENV)?;
SignerLoader::File { key_path } => {
let path = load_env_var(SIGNER_KEYS_ENV).unwrap_or(
key_path.to_str().ok_or_eyre("Missing signer key path")?.to_string(),
);
let file = std::fs::read_to_string(path)
.unwrap_or_else(|_| panic!("Unable to find keys file"));

Expand Down Expand Up @@ -288,6 +290,20 @@ fn load_one(ks_path: String, pw_path: String) -> eyre::Result<ConsensusSigner> {
ConsensusSigner::new_from_bytes(key.sk.serialize().as_bytes())
}

pub fn load_bls_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Result<BlsSigner> {
load_one(keys_path.to_string_lossy().to_string(), secrets_path.to_string_lossy().to_string())
}

pub fn load_ecdsa_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Result<EcdsaSigner> {
let key_file = std::fs::File::open(keys_path.to_string_lossy().to_string())?;
let key_reader = std::io::BufReader::new(key_file);
let keystore: JsonKeystore = serde_json::from_reader(key_reader)?;
let password = std::fs::read(secrets_path)?;
let decrypted_password = eth2_keystore::decrypt(&password, &keystore.crypto).unwrap();

EcdsaSigner::new_from_bytes(decrypted_password.as_bytes())
}

#[cfg(test)]
mod tests {

Expand Down
Loading
Loading