Skip to content

Commit 582fb87

Browse files
authored
feat(signer): add ERC2335 ProxyStore (#193)
* Add ERC2335 ProxyStore type with loader * Small refactor * Store proxy keys and secrets when generated * Add support for ECDSA keys * Refactor * Update docs * Add multiple proxies support for same module * Fix clippy * Add tests * Update docs * Fix parallel tests failures * Run tests sequentially * Fix paths * Read paths from config if env var is not present * Add healthcheck to signer container * Fix format * Update example config * Use signer_port var for healthcheck
1 parent 61e9f27 commit 582fb87

File tree

14 files changed

+682
-23
lines changed

14 files changed

+682
-23
lines changed

config.example.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,17 @@ key_path = "./keys.example.json"
158158
# For teku, it's the path to the directory where all `<pubkey>.txt` files are located.
159159
# For lodestar, it's the path to the file containing the decryption password.
160160
# secrets_path = ""
161-
# Configuration for how the Signer module should store proxy delegations. Currently one type of store is supported:
161+
# Configuration for how the Signer module should store proxy delegations. Supported types of store are:
162162
# - File: store keys and delegations from a plain text file (unsafe, use only for testing purposes)
163+
# - 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)
163164
# OPTIONAL, if missing proxies are lost on restart
164165
[signer.local.store]
165166
# File: path to the keys file
166167
proxy_dir = "./proxies"
168+
# ERC2335: path to the keys directory
169+
# keys_path = "./tests/data/proxy/keys"
170+
# ERC2335: path to the secrets directory
171+
# secrets_path = "./tests/data/proxy/secrets"
167172

168173
# Commit-Boost can optionally run "modules" which extend the capabilities of the sidecar.
169174
# Currently, two types of modules are supported:

crates/cli/src/docker_init.rs

+42-7
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ use cb_common::{
99
CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV,
1010
BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, JWTS_ENV, LOGS_DIR_DEFAULT,
1111
LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV,
12-
PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT,
13-
SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV,
14-
SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV,
12+
PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT,
13+
PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT,
14+
SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT,
15+
SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV,
16+
SIGNER_URL_ENV,
1517
},
1618
signer::{ProxyStore, SignerLoader},
1719
types::ModuleId,
1820
utils::random_jwt,
1921
};
2022
use docker_compose_types::{
21-
Compose, ComposeVolume, DependsOnOptions, EnvFile, Environment, Labels, LoggingParameters,
22-
MapOrEmpty, NetworkSettings, Networks, Ports, Service, Services, SingleValue, TopLevelVolumes,
23-
Volumes,
23+
Compose, ComposeVolume, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck,
24+
HealthcheckTest, Labels, LoggingParameters, MapOrEmpty, NetworkSettings, Networks, Ports,
25+
Service, Services, SingleValue, TopLevelVolumes, Volumes,
2426
};
2527
use eyre::Result;
2628
use indexmap::IndexMap;
@@ -151,6 +153,12 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
151153
module_volumes.extend(chain_spec_volume.clone());
152154
module_volumes.extend(get_log_volume(&cb_config.logs, &module.id));
153155

156+
// depends_on
157+
let mut module_dependencies = IndexMap::new();
158+
module_dependencies.insert("cb_signer".into(), DependsCondition {
159+
condition: "service_healthy".into(),
160+
});
161+
154162
Service {
155163
container_name: Some(module_cid.clone()),
156164
image: Some(module.docker_image),
@@ -160,7 +168,7 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
160168
depends_on: if let Some(SignerConfig::Remote { .. }) = &cb_config.signer {
161169
DependsOnOptions::Simple(vec![])
162170
} else {
163-
DependsOnOptions::Simple(vec!["cb_signer".to_owned()])
171+
DependsOnOptions::Conditional(module_dependencies)
164172
},
165173
env_file,
166174
..Service::default()
@@ -367,6 +375,23 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
367375
let (k, v) = get_env_val(PROXY_DIR_ENV, PROXY_DIR_DEFAULT);
368376
signer_envs.insert(k, v);
369377
}
378+
ProxyStore::ERC2335 { keys_path, secrets_path } => {
379+
volumes.push(Volumes::Simple(format!(
380+
"{}:{}:rw",
381+
keys_path.display(),
382+
PROXY_DIR_KEYS_DEFAULT
383+
)));
384+
let (k, v) = get_env_val(PROXY_DIR_KEYS_ENV, PROXY_DIR_KEYS_DEFAULT);
385+
signer_envs.insert(k, v);
386+
387+
volumes.push(Volumes::Simple(format!(
388+
"{}:{}:rw",
389+
secrets_path.display(),
390+
PROXY_DIR_SECRETS_DEFAULT
391+
)));
392+
let (k, v) = get_env_val(PROXY_DIR_SECRETS_ENV, PROXY_DIR_SECRETS_DEFAULT);
393+
signer_envs.insert(k, v);
394+
}
370395
}
371396
}
372397

@@ -384,6 +409,16 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()>
384409
networks: Networks::Simple(signer_networks),
385410
volumes,
386411
environment: Environment::KvPair(signer_envs),
412+
healthcheck: Some(Healthcheck {
413+
test: Some(HealthcheckTest::Single(format!(
414+
"curl -f http://localhost:{signer_port}/status"
415+
))),
416+
interval: Some("5s".into()),
417+
timeout: Some("5s".into()),
418+
retries: 5,
419+
start_period: Some("0s".into()),
420+
disable: false,
421+
}),
387422
..Service::default()
388423
};
389424

crates/common/src/commit/constants.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub const GET_PUBKEYS_PATH: &str = "/signer/v1/get_pubkeys";
22
pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature";
33
pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key";
4+
pub const STATUS_PATH: &str = "/status";

crates/common/src/commit/request.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::fmt::{self, Debug, Display, LowerHex};
1+
use std::{
2+
fmt::{self, Debug, Display, LowerHex},
3+
str::FromStr,
4+
};
25

36
use alloy::rpc::types::beacon::BlsSignature;
47
use derive_more::derive::From;
@@ -133,6 +136,27 @@ pub enum EncryptionScheme {
133136
Ecdsa,
134137
}
135138

139+
impl Display for EncryptionScheme {
140+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141+
match self {
142+
EncryptionScheme::Bls => write!(f, "bls"),
143+
EncryptionScheme::Ecdsa => write!(f, "ecdsa"),
144+
}
145+
}
146+
}
147+
148+
impl FromStr for EncryptionScheme {
149+
type Err = String;
150+
151+
fn from_str(s: &str) -> Result<Self, Self::Err> {
152+
match s {
153+
"bls" => Ok(EncryptionScheme::Bls),
154+
"ecdsa" => Ok(EncryptionScheme::Ecdsa),
155+
_ => Err(format!("Unknown scheme: {s}")),
156+
}
157+
}
158+
}
159+
136160
// TODO(David): This struct shouldn't be visible to module authors
137161
#[derive(Debug, Clone, Serialize, Deserialize)]
138162
pub struct GenerateProxyRequest {

crates/common/src/config/constants.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ pub const SIGNER_DIR_KEYS_DEFAULT: &str = "/keys";
4747
/// Path to `secrets` folder
4848
pub const SIGNER_DIR_SECRETS_ENV: &str = "CB_SIGNER_LOADER_SECRETS_DIR";
4949
pub const SIGNER_DIR_SECRETS_DEFAULT: &str = "/secrets";
50-
/// Path to store proxies
50+
/// Path to store proxies with plaintext keys (testing only)
5151
pub const PROXY_DIR_ENV: &str = "CB_PROXY_STORE_DIR";
5252
pub const PROXY_DIR_DEFAULT: &str = "/proxies";
53+
/// Path to store proxy keys
54+
pub const PROXY_DIR_KEYS_ENV: &str = "CB_PROXY_KEYS_DIR";
55+
pub const PROXY_DIR_KEYS_DEFAULT: &str = "/proxy_keys";
56+
/// Path to store proxy secrets
57+
pub const PROXY_DIR_SECRETS_ENV: &str = "CB_PROXY_SECRETS_DIR";
58+
pub const PROXY_DIR_SECRETS_DEFAULT: &str = "/proxy_secrets";
5359

5460
///////////////////////// MODULES /////////////////////////
5561

crates/common/src/signer/loader.rs

+20-4
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ use aes::{
1010
Aes128,
1111
};
1212
use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey};
13-
use eth2_keystore::Keystore;
13+
use eth2_keystore::{json_keystore::JsonKeystore, Keystore};
1414
use eyre::{eyre, Context, OptionExt};
1515
use pbkdf2::{hmac, pbkdf2};
1616
use serde::{de, Deserialize, Deserializer, Serialize};
1717
use tracing::warn;
1818
use unicode_normalization::UnicodeNormalization;
1919

20-
use super::{PrysmDecryptedKeystore, PrysmKeystore};
20+
use super::{BlsSigner, EcdsaSigner, PrysmDecryptedKeystore, PrysmKeystore};
2121
use crate::{
2222
config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV},
2323
signer::ConsensusSigner,
@@ -56,8 +56,10 @@ impl SignerLoader {
5656

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

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

293+
pub fn load_bls_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Result<BlsSigner> {
294+
load_one(keys_path.to_string_lossy().to_string(), secrets_path.to_string_lossy().to_string())
295+
}
296+
297+
pub fn load_ecdsa_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Result<EcdsaSigner> {
298+
let key_file = std::fs::File::open(keys_path.to_string_lossy().to_string())?;
299+
let key_reader = std::io::BufReader::new(key_file);
300+
let keystore: JsonKeystore = serde_json::from_reader(key_reader)?;
301+
let password = std::fs::read(secrets_path)?;
302+
let decrypted_password = eth2_keystore::decrypt(&password, &keystore.crypto).unwrap();
303+
304+
EcdsaSigner::new_from_bytes(decrypted_password.as_bytes())
305+
}
306+
291307
#[cfg(test)]
292308
mod tests {
293309

0 commit comments

Comments
 (0)