diff --git a/Cargo.lock b/Cargo.lock index a03ee6f2..d9e35119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,12 +24,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", - "cipher", + "cipher 0.3.0", "cpufeatures", - "ctr", + "ctr 0.8.0", "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -1134,20 +1145,26 @@ dependencies = [ name = "cb-common" version = "0.3.1" dependencies = [ + "aes 0.8.4", "alloy", "axum", + "base64 0.22.1", "bimap", "blst", + "cipher 0.4.4", + "ctr 0.9.2", "derive_more", "eth2_keystore", "ethereum_serde_utils 0.7.0", "eyre", "k256", + "pbkdf2 0.12.2", "rand", "reqwest", "serde", "serde_json", "serde_yaml", + "sha2 0.10.8", "ssz_types", "thiserror", "tokio", @@ -1157,6 +1174,7 @@ dependencies = [ "tracing-subscriber", "tree_hash 0.8.0", "tree_hash_derive", + "unicode-normalization", "url", ] @@ -1272,6 +1290,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.4" @@ -1500,7 +1528,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" dependencies = [ - "cipher", + "cipher 0.3.0", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", ] [[package]] @@ -1801,12 +1838,12 @@ name = "eth2_keystore" version = "0.1.0" source = "git+https://github.com/sigp/lighthouse?rev=9e12c21f268c80a3f002ae0ca27477f9f512eb6f#9e12c21f268c80a3f002ae0ca27477f9f512eb6f" dependencies = [ - "aes", + "aes 0.7.5", "bls", "eth2_key_derivation", "hex", "hmac 0.11.0", - "pbkdf2", + "pbkdf2 0.8.0", "rand", "scrypt", "serde", @@ -2519,6 +2556,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2959,6 +3005,16 @@ dependencies = [ "crypto-mac", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3463,7 +3519,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbd2eb639fd7cab5804a0837fe373cc2172d15437e804c054a9fb885cb923b0" dependencies = [ - "cipher", + "cipher 0.3.0", ] [[package]] @@ -3499,7 +3555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" dependencies = [ "hmac 0.11.0", - "pbkdf2", + "pbkdf2 0.8.0", "salsa20", "sha2 0.9.9", ] @@ -4397,9 +4453,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] diff --git a/Cargo.toml b/Cargo.toml index 91198e15..b1457fdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,8 @@ toml = "0.8.13" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.33" +base64 = "0.22.1" +unicode-normalization = "0.1.24" # telemetry tracing = "0.1.40" @@ -68,6 +70,11 @@ tree_hash = "0.8" tree_hash_derive = "0.8" eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "9e12c21f268c80a3f002ae0ca27477f9f512eb6f" } k256 = "0.13" +aes = "0.8" +ctr = "0.9.2" +cipher = "0.4" +pbkdf2 = "0.12.2" +sha2 = "0.10.8" # docker docker-compose-types = "0.12.0" diff --git a/config.example.toml b/config.example.toml index 86110b5b..5e572983 100644 --- a/config.example.toml +++ b/config.example.toml @@ -125,13 +125,23 @@ headers = { X-MyCustomHeader = "ADifferentCustomValue" } docker_image = "ghcr.io/commit-boost/signer:latest" # Configuration for how the Signer module should load validator keys. Currently two types of loaders are supported: # - File: load keys from a plain text file (unsafe, use only for testing purposes) -# - ValidatorsDir: load keys from a `keys` and `secrets` folder (ERC-2335 style keystores as used in Lighthouse) +# - ValidatorsDir: load keys from a `keys` and `secrets` file/folder (ERC-2335 style keystores). More details can be found in the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/) [signer.loader] # File: path to the keys file key_path = "./keys.example.json" -# ValidatorsDir: path to the keys directory +# ValidatorsDir: format of the keystore (lighthouse, prysm, teku or lodestar) +# format = "lighthouse" +# ValidatorsDir: full path to the keys directory +# For lighthouse, it's de path to the directory where the `/voting-keystore.json` directories are located. +# For prysm, it's the path to the `all-accounts.keystore.json` file. +# For teku, it's the path to the directory where all `.json` files are located. +# For lodestar, it's the path to the directory where all `.json` files are located. # keys_path = "" -# ValidatorsDir: path to the secrets directory +# ValidatorsDir: full path to the secrets file/directory +# For lighthouse, it's de path to the directory where the `.json` files are located. +# For prysm, it's the path to the file containing the wallet decryption password. +# For teku, it's the path to the directory where all `.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: # - File: store keys and delegations from a plain text file (unsafe, use only for testing purposes) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 8da6e7ea..81482bbc 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -319,7 +319,7 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> let (k, v) = get_env_val(SIGNER_KEYS_ENV, SIGNER_DEFAULT); signer_envs.insert(k, v); } - SignerLoader::ValidatorsDir { keys_path, secrets_path } => { + SignerLoader::ValidatorsDir { keys_path, secrets_path, format: _ } => { volumes.push(Volumes::Simple(format!( "{}:{}:ro", keys_path.display(), diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 90e5df64..4f2b521b 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -35,6 +35,11 @@ tree_hash.workspace = true tree_hash_derive.workspace = true eth2_keystore.workspace = true k256.workspace = true +aes.workspace = true +ctr.workspace = true +cipher.workspace = true +pbkdf2.workspace = true +sha2.workspace = true # misc thiserror.workspace = true @@ -43,3 +48,6 @@ url.workspace = true rand.workspace = true bimap.workspace = true derive_more.workspace = true + +unicode-normalization.workspace = true +base64.workspace = true diff --git a/crates/common/src/signer/loader.rs b/crates/common/src/signer/loader.rs index c06f6716..ebdcc13a 100644 --- a/crates/common/src/signer/loader.rs +++ b/crates/common/src/signer/loader.rs @@ -1,11 +1,23 @@ -use std::{fs, path::PathBuf}; +use std::{ + ffi::OsStr, + fs::{self, File}, + io::BufReader, + path::PathBuf, +}; +use aes::{ + cipher::{KeyIvInit, StreamCipher}, + Aes128, +}; use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey}; use eth2_keystore::Keystore; -use eyre::{eyre, Context}; +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 crate::{ config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV}, signer::ConsensusSigner, @@ -21,9 +33,22 @@ pub enum SignerLoader { ValidatorsDir { keys_path: PathBuf, secrets_path: PathBuf, + format: ValidatorKeysFormat, }, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ValidatorKeysFormat { + #[serde(alias = "lighthouse")] + Lighthouse, + #[serde(alias = "teku")] + Teku, + #[serde(alias = "lodestar")] + Lodestar, + #[serde(alias = "prysm")] + Prysm, +} + impl SignerLoader { pub fn load_keys(self) -> eyre::Result> { self.load_from_env() @@ -43,12 +68,26 @@ impl SignerLoader { .collect::>() .context("failed to load signers")? } - SignerLoader::ValidatorsDir { .. } => { + SignerLoader::ValidatorsDir { keys_path, secrets_path, format } => { // 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).context("failed to load signers")? + let keys_path = load_env_var(SIGNER_DIR_KEYS_ENV).unwrap_or( + keys_path.to_str().ok_or_eyre("Missing signer keys path")?.to_string(), + ); + let secrets_path = load_env_var(SIGNER_DIR_SECRETS_ENV).unwrap_or( + secrets_path.to_str().ok_or_eyre("Missing signer secrets path")?.to_string(), + ); + + return match format { + ValidatorKeysFormat::Lighthouse => { + load_from_lighthouse_format(keys_path, secrets_path) + } + ValidatorKeysFormat::Teku => load_from_teku_format(keys_path, secrets_path), + ValidatorKeysFormat::Lodestar => { + load_from_lodestar_format(keys_path, secrets_path) + } + ValidatorKeysFormat::Prysm => load_from_prysm_format(keys_path, secrets_path), + }; } }) } @@ -72,7 +111,7 @@ impl<'de> Deserialize<'de> for FileKey { } } -fn load_secrets_and_keys( +fn load_from_lighthouse_format( keys_path: String, secrets_path: String, ) -> eyre::Result> { @@ -105,9 +144,145 @@ fn load_secrets_and_keys( Ok(signers) } +fn load_from_teku_format( + keys_path: String, + secrets_path: String, +) -> eyre::Result> { + let entries = fs::read_dir(keys_path.clone())?; + let mut signers = Vec::new(); + + for entry in entries { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + warn!("Path {path:?} is a dir"); + continue; + } + + let file_name = path + .file_name() + .and_then(OsStr::to_str) + .ok_or_eyre("File name not valid")? + .rsplit_once(".") + .ok_or_eyre("File doesn't have extension")? + .0; + + match load_one( + format!("{keys_path}/{file_name}.json"), + format!("{secrets_path}/{file_name}.txt"), + ) { + Ok(signer) => signers.push(signer), + Err(e) => warn!("Sign load error: {e}"), + } + } + + Ok(signers) +} + +fn load_from_lodestar_format( + keys_path: String, + password_path: String, +) -> eyre::Result> { + let entries = fs::read_dir(keys_path)?; + let mut signers = Vec::new(); + + for entry in entries { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + warn!("Path {path:?} is a dir"); + continue; + } + + let key_path = match path.as_os_str().to_str() { + Some(key_path) => key_path, + None => { + warn!("Path {path:?} cannot be converted to string"); + continue; + } + }; + + match load_one(key_path.to_string(), password_path.clone()) { + Ok(signer) => signers.push(signer), + Err(e) => warn!("Sign load error: {e}"), + } + } + + Ok(signers) +} + +/// Prysm's keystore is a json file with the keys encrypted with a password, +/// among with some metadata to decrypt them. +/// Once decrypted, the keys have the following structure: +/// ```json +/// { +/// "private_keys": [ +/// "sk1_base64_encoded", +/// "sk2_base64_encoded", +/// ... +/// ], +/// "public_keys": [ +/// "pk1_base64_encoded", +/// "pk2_base64_encoded", +/// ... +/// ] +/// } +/// ``` +fn load_from_prysm_format( + accounts_path: String, + password_path: String, +) -> eyre::Result> { + let accounts_file = File::open(accounts_path)?; + let accounts_reader = BufReader::new(accounts_file); + let keystore: PrysmKeystore = + serde_json::from_reader(accounts_reader).map_err(|e| eyre!("Failed reading json: {e}"))?; + + let password = fs::read_to_string(password_path)?; + // Normalized as required by EIP-2335 + // (https://eips.ethereum.org/EIPS/eip-2335#password-requirements) + let normalized_password = password + .nfkd() + .collect::() + .bytes() + .filter(|char| (*char > 0x1F && *char < 0x7F) || *char > 0x9F) + .collect::>(); + + let mut decryption_key = [0u8; 32]; + pbkdf2::>( + &normalized_password, + &keystore.salt, + keystore.c, + &mut decryption_key, + )?; + + let ciphertext = keystore.message; + + let mut cipher = ctr::Ctr128BE::::new_from_slices(&decryption_key[..16], &keystore.iv) + .map_err(|_| eyre!("Invalid key or nonce"))?; + + let mut buf = vec![0u8; ciphertext.len()].into_boxed_slice(); + cipher + .apply_keystream_b2b(&ciphertext, &mut buf) + .map_err(|_| eyre!("Failed decrypting accounts"))?; + + let decrypted_keystore: PrysmDecryptedKeystore = + serde_json::from_slice(&buf).map_err(|e| eyre!("Failed reading json: {e}"))?; + let mut signers = Vec::with_capacity(decrypted_keystore.private_keys.len()); + + for key in decrypted_keystore.private_keys { + let signer = ConsensusSigner::new_from_bytes(&key)?; + signers.push(signer); + } + + Ok(signers) +} + 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 password = + fs::read(pw_path.clone()).map_err(|e| eyre!("Failed to read password ({pw_path}): {e}"))?; let key = keystore.decrypt_keypair(&password).map_err(|_| eyre!("failed decrypting keypair"))?; ConsensusSigner::new_from_bytes(key.sk.serialize().as_bytes()) @@ -116,7 +291,13 @@ fn load_one(ks_path: String, pw_path: String) -> eyre::Result { #[cfg(test)] mod tests { - use super::FileKey; + use alloy::{hex, primitives::FixedBytes}; + + use super::{load_from_lighthouse_format, load_from_lodestar_format, FileKey}; + use crate::signer::{ + loader::{load_from_prysm_format, load_from_teku_format}, + BlsPublicKey, BlsSigner, + }; #[test] fn test_decode() { @@ -133,4 +314,81 @@ mod tests { assert_eq!(decoded[0].secret_key, s) } + + fn test_correct_load(signers: Vec) { + assert_eq!(signers.len(), 2); + assert!(signers.iter().any(|s| s.pubkey() == BlsPublicKey::from(FixedBytes::new( + hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4") + )))); + assert!(signers.iter().any(|s| s.pubkey() == BlsPublicKey::from(FixedBytes::new( + hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9") + )))); + } + + #[test] + fn test_load_lighthouse() { + let result = load_from_lighthouse_format( + "../../tests/data/keystores/keys".into(), + "../../tests/data/keystores/secrets".into(), + ); + + assert!(result.is_ok()); + + test_correct_load(result.unwrap()); + } + + #[test] + fn test_load_teku() { + let result = load_from_teku_format( + "../../tests/data/keystores/teku-keys".into(), + "../../tests/data/keystores/teku-secrets".into(), + ); + + assert!(result.is_ok()); + + test_correct_load(result.unwrap()); + } + + #[test] + fn test_load_prysm() { + let result = load_from_prysm_format( + "../../tests/data/keystores/prysm/direct/accounts/all-accounts.keystore.json".into(), + "../../tests/data/keystores/prysm/empty_pass".into(), + ); + + assert!(result.is_ok()); + + test_correct_load(result.unwrap()); + } + + #[test] + fn test_load_lodestar() { + let result = load_from_lodestar_format( + "../../tests/data/keystores/teku-keys/".into(), + "../../tests/data/keystores/secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4".into() + ); + + assert!(result.is_ok()); + + let signers = result.unwrap(); + + assert_eq!(signers.len(), 1); + assert!(signers[0].pubkey() == BlsPublicKey::from(FixedBytes::new( + hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4") + ))); + + let result = load_from_lodestar_format( + "../../tests/data/keystores/teku-keys/".into(), + "../../tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9".into() + ); + + assert!(result.is_ok()); + + let signers = result.unwrap(); + + assert_eq!(signers.len(), 1); + assert!(signers[0].pubkey() == BlsPublicKey::from(FixedBytes::new( + hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9") + ))); + } } diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index bb1bfc9e..4071f858 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -1,6 +1,12 @@ use std::collections::HashMap; +use alloy::primitives::Bytes; +use base64::{prelude::BASE64_STANDARD, Engine}; use derive_more::derive::Deref; +use serde::{ + de::{Error as DeError, Unexpected}, + Deserialize, Deserializer, +}; use super::{BlsPublicKey, EcdsaPublicKey, EcdsaSigner}; use crate::{ @@ -33,3 +39,86 @@ pub struct ProxySigners { pub bls_signers: HashMap, pub ecdsa_signers: HashMap, } + +// Prysm keystore actually has a more complex structure, but we only need +// this subset of fields +pub struct PrysmKeystore { + pub message: Bytes, + pub salt: Bytes, + pub c: u32, + pub iv: Bytes, +} + +#[derive(Deserialize, Debug)] +pub struct PrysmDecryptedKeystore { + #[serde(deserialize_with = "base64_list_decode")] + pub private_keys: Vec, + #[serde(deserialize_with = "base64_list_decode")] + pub public_keys: Vec, +} + +fn base64_list_decode<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let list: Vec<&str> = Deserialize::deserialize(deserializer)?; + let mut decoded_list = Vec::with_capacity(list.len()); + + for encoded_key in list.iter() { + decoded_list.push( + BASE64_STANDARD + .decode(encoded_key) + .map_err(|_| DeError::invalid_type(Unexpected::Other("unknown"), &"base64 string"))? + .into(), + ); + } + + Ok(decoded_list) +} + +// impl serde deserialize for PrysmKeystore: +impl<'de> Deserialize<'de> for PrysmKeystore { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: serde_json::Value = Deserialize::deserialize(deserializer)?; + let crypto = value.get("crypto").ok_or(DeError::missing_field("crypto"))?; + let cipher = crypto.get("cipher").ok_or(DeError::missing_field("crypto.cipher"))?; + let kdf_params = crypto + .get("kdf") + .ok_or(DeError::missing_field("kdf"))? + .get("params") + .ok_or(DeError::missing_field("kdf.params"))?; + + Ok(PrysmKeystore { + message: serde_json::from_value( + cipher + .get("message") + .ok_or(DeError::missing_field("crypto.cipher.message"))? + .clone(), + ) + .map_err(|_| DeError::invalid_type(Unexpected::Other("unknown"), &"bytes"))?, + salt: serde_json::from_value( + kdf_params + .get("salt") + .ok_or(DeError::missing_field("crypto.kdf.params.salt"))? + .clone(), + ) + .map_err(|_| DeError::invalid_type(Unexpected::Other("unknown"), &"bytes"))?, + c: serde_json::from_value( + kdf_params.get("c").ok_or(DeError::missing_field("crypto.kdf.params.c"))?.clone(), + ) + .map_err(|_| DeError::invalid_type(Unexpected::Other("unknown"), &"u32"))?, + iv: serde_json::from_value( + cipher + .get("params") + .ok_or(DeError::missing_field("crypto.cipher.params"))? + .get("iv") + .ok_or(DeError::missing_field("crypto.cipher.params.iv"))? + .clone(), + ) + .map_err(|_| DeError::invalid_type(Unexpected::Other("unknown"), &"bytes"))?, + }) + } +} diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 0fd55179..53e8b462 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -29,12 +29,124 @@ After the sidecar is started, it will expose a port (`18550` in this example), t Note that in this setup, the signer module will not be started. +## Signer module + +To start the signer module, you need to include its parameters in the config file: + +```toml +[signer] +[signer.loader] +format = "lighthouse" +keys_path = "/path/to/keys" +secrets_path = "/path/to.secrets" +``` + +We currently support Lighthouse, Prysm, Teku and Lodestar's keystores so it's easier to load the keys. We're working on adding support for additional keystores, including remote signers. These are the expected file structures for each format: + +
+ Lighthouse + + #### File structure: + ``` + ├── keys + │   ├── + │   │   └── voting-keystore.json + │   └── + │   └── voting-keystore.json + └── secrets +    ├── +    └── + ``` + + #### Config: + ```toml + [signer] + [signer.loader] + format = "lighthouse" + keys_path = "keys" + secrets_path = "secrets" + ``` +
+ +
+ Prysm + + #### File structure: + ``` + ├── wallet + │   └── direct + │      └── accounts + │         └──all-accounts.keystore.json + └── secrets +    └── password.txt + ``` + + #### Config: + ```toml + [signer] + [signer.loader] + format = "prysm" + keys_path = "wallet/direct/accounts/all-accounts.keystore.json" + secrets_path = "secrets/password.txt" + ``` +
+ +
+ Teku + + #### File structure: + ``` + ├── keys + │   ├── .json + │   └── .json + └── secrets +    ├── .txt +    └── .txt + ``` + + #### Config: + ```toml + [signer] + [signer.loader] + format = "teku" + keys_path = "keys" + secrets_path = "secrets" + ``` +
+ +
+ Lodestar + + #### File structure: + ``` + ├── keys + │   ├── .json + │   └── .json + └── secrets +    └── password.txt + ``` + + #### Config: + ```toml + [signer] + [signer.loader] + format = "lodestar" + keys_path = "keys" + secrets_path = "secrets/password.txt" + ``` + + :::note + All keys have the same password stored in `secrets/password.txt` + ::: +
+ + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: ```bash bash scripts/build_local_modules.sh ``` -This will create a Docker image called `test_da_commit` that periodically requests signatures from the validator, and a `test_builder_log` module that logs BuilderAPI events. +This will create a Docker image called `test_da_commit` that periodically requests signatures from the validator, and a `test_builder_log` module that logs BuilderAPI events. The `cb-config.toml` file needs to be updated as follows: ```toml @@ -46,6 +158,7 @@ url = "" [signer] [signer.loader] +format = "lighthouse" keys_path = "/path/to/keys" secrets_path = "/path/to.secrets" @@ -65,7 +178,7 @@ docker_image = "test_builder_log" ``` A few things to note: -- We now added a `signer` section which will be used to create the Signer module. To load keys in the module, we currently support the Lighthouse `validators_dir` keys and secrets. We're working on adding support for additional keystores, including remote signers. +- We now added a `signer` section which will be used to create the Signer module. - There is now a `[[modules]]` section which at a minimum needs to specify the module `id`, `type` and `docker_image`. Additional parameters needed for the business logic of the module will also be here, To learn more about developing modules, check out [here](/category/developing). @@ -80,7 +193,7 @@ You can setup Commit-Boost with Vouch in two ways. For simplicity, assume that in Vouch `blockrelay.listen-address: 127.0.0.0:19550` and in Commit-Boost `pbs.port = 18550`. #### Beacon Node to Vouch -In this setup, the BN Builder-API endpoint will be pointing to the Vouch `blockrelay` (e.g. for Lighthouse you will need the flag `--builder=http://127.0.0.0:19550`). +In this setup, the BN Builder-API endpoint will be pointing to the Vouch `blockrelay` (e.g. for Lighthouse you will need the flag `--builder=http://127.0.0.0:19550`). Modify the `blockrelay.config` file to add Commit-Boost: ```json @@ -90,7 +203,7 @@ Modify the `blockrelay.config` file to add Commit-Boost: ``` #### Beacon Node to Commit Boost -In this setup, the BN Builder-API endpoint will be pointing to the PBS module (e.g. for Lighthouse you will need the flag `--builder=http://127.0.0.0:18550`). +In this setup, the BN Builder-API endpoint will be pointing to the PBS module (e.g. for Lighthouse you will need the flag `--builder=http://127.0.0.0:18550`). This will bypass the `blockrelay` entirely so make sure all relays are properly configured in the `[[relays]]` section. @@ -99,5 +212,3 @@ This will bypass the `blockrelay` entirely so make sure all relays are properly ### Notes - It's up to you to decide which relays will be connected via Commit-Boost (`[[relays]]` section in the `toml` config) and which via Vouch (additional entries in the `relays` field). Remember that any rate-limit will be shared across the two sidecars, if running on the same machine. - You may occasionally see a `timeout` error during registrations, especially if you're running a large number of validators in the same instance. This can resolve itself as registrations will be cleared later in the epoch when relays are less busy processing other registrations. Alternatively you can also adjust the `builderclient.timeout` option in `.vouch.yml`. - - diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 0f9339fc..a2dc2402 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -28,7 +28,7 @@ Modules need some environment variables to work correctly. - `CB_SIGNER_PORT`: required, port to open the signer server on For loading keys we currently support: - `CB_SIGNER_LOADER_FILE`: path to a `.json` with plaintext keys (for testing purposes only) - - `CB_SIGNER_LOADER_KEYS_DIR` and `CB_SIGNER_LOADER_SECRETS_DIR`: paths to the `keys` and `secrets` directories (ERC-2335 style keystores as used in Lighthouse) + - `CB_SIGNER_LOADER_FORMAT`, `CB_SIGNER_LOADER_KEYS_DIR` and `CB_SIGNER_LOADER_SECRETS_DIR`: paths to the `keys` and `secrets` directories or files (ERC-2335 style keystores, see [Signer config](../configuration/#signer-module) for more info) For storing proxy keys we currently support: - `CB_PROXY_STORE_DIR`: directory where proxy keys and delegations will be saved in plaintext (for testing purposes only) @@ -54,5 +54,4 @@ CB_CONFIG=./cb-config.toml commit-boost-pbs ``` ## Security -Running the modules natively means you opt out of the security guarantees made by Docker and it's up to you how to setup and ensure the modules run safely. - +Running the modules natively means you opt out of the security guarantees made by Docker and it's up to you how to setup and ensure the modules run safely. diff --git a/tests/data/keystores/keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4/voting-keystore.json b/tests/data/keystores/keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4/voting-keystore.json new file mode 100644 index 00000000..72b13cad --- /dev/null +++ b/tests/data/keystores/keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4/voting-keystore.json @@ -0,0 +1 @@ +{"crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"0ded1a0ed9d0d5aa9c41ac1a6be6d9943835f9ccbe1081869af74925611a4687"},"message":""},"checksum":{"function":"sha256","params":{},"message":"b1de458543b0532666e8f24e679f93ed6f168fd09de1da7c3f4f79b7fa2f2412"},"cipher":{"function":"aes-128-ctr","params":{"iv":"3ca34eb318e53a4c7e545571d8d0c7af"},"message":"acc6c222eea80974107b5a9bf824c8156edaad944f0d444a1aab4cc2118cecc5"}},"description":"0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4","pubkey":"883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4","path":"","uuid":"61c06c9c-b0bc-4022-9bf8-a2f250d4e751","version":4} \ No newline at end of file diff --git a/tests/data/keystores/keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9/voting-keystore.json b/tests/data/keystores/keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9/voting-keystore.json new file mode 100644 index 00000000..ba717c1c --- /dev/null +++ b/tests/data/keystores/keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9/voting-keystore.json @@ -0,0 +1 @@ +{"crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"2154bba4d5999c6069442db5b499b2b27b6c2f54f36490e51163934dd4fb412e"},"message":""},"checksum":{"function":"sha256","params":{},"message":"1db4975098c97905f1dd9a9207cab0a9af7e16bebdab700ee08efb51e068017f"},"cipher":{"function":"aes-128-ctr","params":{"iv":"2265a3b57110b46c08295e53379165b5"},"message":"3bd312cc34cebfdd890c9704752191ed93ecd562bb62d2d8ceb4ff945b58b790"}},"description":"0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9","pubkey":"b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9","path":"","uuid":"a8457299-739d-42fb-a0f6-961020f22b8e","version":4} \ No newline at end of file diff --git a/tests/data/keystores/lodestar-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 b/tests/data/keystores/lodestar-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 new file mode 100644 index 00000000..88a84e76 --- /dev/null +++ b/tests/data/keystores/lodestar-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 @@ -0,0 +1 @@ +2MtI__9JSKFcN2Syqpdy5MmM8RXZbM26Pel7G1HCuIg= \ No newline at end of file diff --git a/tests/data/keystores/lodestar-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 b/tests/data/keystores/lodestar-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 new file mode 100644 index 00000000..b2ce4dfd --- /dev/null +++ b/tests/data/keystores/lodestar-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 @@ -0,0 +1 @@ +BWBoV1UZpkO4cUA-t8T9aViJ0sBfilR7qJFHgU4tBSc= \ No newline at end of file diff --git a/tests/data/keystores/prysm/direct/accounts/all-accounts.keystore.json b/tests/data/keystores/prysm/direct/accounts/all-accounts.keystore.json new file mode 100644 index 00000000..40d189bb --- /dev/null +++ b/tests/data/keystores/prysm/direct/accounts/all-accounts.keystore.json @@ -0,0 +1,29 @@ +{ + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "0e538586adf998caa12c7a42772cb559ccb49e69c71159d924f0ade3e4a86240" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "da07b64a482f95c322b6c506dea20f53007391bc7c60255e480fef5994d6d826" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "7180c42635fb41584db7b9f14264b504" + }, + "message": "11d4016d0893228d09e14d9d354a6d8a5c280eefbb8277c36b281a95dfe9a5c506ae8538f6a25799d1c16c32319bb126ceff4c09a3de5ec355ed8e1c5662e1942e2b32a28977c59ed9a7e3d8756e69b3862dd03f38391ae110f48b0b3520c715633afb7ed62fc6ec9b41b4318e629da6b44ed216b4de02b05b2b0224c083f5ec932980a8d13672562a73bead88b61760753bff91a484dfdc50442686ee054894a61b072c52c934d0763c9502f9988b10f1a50176a2d2a9ba2186d620faa9f97be4762be86da03fa2209c9c7c1974158539a7835b8426225ff6ff173790c55a304282b9a8991ddc5cb9c6e7e7e1cd7ec75e02deeb9b82e0dcfed874fe58fb7bf8a027f9bc127e1d9472afc27ac34575dcb67cc71522ca0c915ba023224a" + } + }, + "path": "", + "uuid": "7d7e3a49-c4ca-4d0a-a0e6-cb199dd72a85", + "version": 4 +} \ No newline at end of file diff --git a/tests/data/keystores/prysm/empty_pass b/tests/data/keystores/prysm/empty_pass new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/keystores/prysm/keymanageropts.json b/tests/data/keystores/prysm/keymanageropts.json new file mode 100644 index 00000000..13c0529c --- /dev/null +++ b/tests/data/keystores/prysm/keymanageropts.json @@ -0,0 +1 @@ +{"direct_eip_version": "EIP-2335"} \ No newline at end of file diff --git a/tests/data/keystores/pubkeys.json b/tests/data/keystores/pubkeys.json new file mode 100644 index 00000000..eca508ca --- /dev/null +++ b/tests/data/keystores/pubkeys.json @@ -0,0 +1 @@ +["0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9","0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4"] \ No newline at end of file diff --git a/tests/data/keystores/secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 b/tests/data/keystores/secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 new file mode 100644 index 00000000..88a84e76 --- /dev/null +++ b/tests/data/keystores/secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4 @@ -0,0 +1 @@ +2MtI__9JSKFcN2Syqpdy5MmM8RXZbM26Pel7G1HCuIg= \ No newline at end of file diff --git a/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 b/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 new file mode 100644 index 00000000..b2ce4dfd --- /dev/null +++ b/tests/data/keystores/secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9 @@ -0,0 +1 @@ +BWBoV1UZpkO4cUA-t8T9aViJ0sBfilR7qJFHgU4tBSc= \ No newline at end of file diff --git a/tests/data/keystores/teku-keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.json b/tests/data/keystores/teku-keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.json new file mode 100644 index 00000000..72b13cad --- /dev/null +++ b/tests/data/keystores/teku-keys/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.json @@ -0,0 +1 @@ +{"crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"0ded1a0ed9d0d5aa9c41ac1a6be6d9943835f9ccbe1081869af74925611a4687"},"message":""},"checksum":{"function":"sha256","params":{},"message":"b1de458543b0532666e8f24e679f93ed6f168fd09de1da7c3f4f79b7fa2f2412"},"cipher":{"function":"aes-128-ctr","params":{"iv":"3ca34eb318e53a4c7e545571d8d0c7af"},"message":"acc6c222eea80974107b5a9bf824c8156edaad944f0d444a1aab4cc2118cecc5"}},"description":"0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4","pubkey":"883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4","path":"","uuid":"61c06c9c-b0bc-4022-9bf8-a2f250d4e751","version":4} \ No newline at end of file diff --git a/tests/data/keystores/teku-keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.json b/tests/data/keystores/teku-keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.json new file mode 100644 index 00000000..ba717c1c --- /dev/null +++ b/tests/data/keystores/teku-keys/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.json @@ -0,0 +1 @@ +{"crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"2154bba4d5999c6069442db5b499b2b27b6c2f54f36490e51163934dd4fb412e"},"message":""},"checksum":{"function":"sha256","params":{},"message":"1db4975098c97905f1dd9a9207cab0a9af7e16bebdab700ee08efb51e068017f"},"cipher":{"function":"aes-128-ctr","params":{"iv":"2265a3b57110b46c08295e53379165b5"},"message":"3bd312cc34cebfdd890c9704752191ed93ecd562bb62d2d8ceb4ff945b58b790"}},"description":"0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9","pubkey":"b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9","path":"","uuid":"a8457299-739d-42fb-a0f6-961020f22b8e","version":4} \ No newline at end of file diff --git a/tests/data/keystores/teku-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.txt b/tests/data/keystores/teku-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.txt new file mode 100644 index 00000000..88a84e76 --- /dev/null +++ b/tests/data/keystores/teku-secrets/0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4.txt @@ -0,0 +1 @@ +2MtI__9JSKFcN2Syqpdy5MmM8RXZbM26Pel7G1HCuIg= \ No newline at end of file diff --git a/tests/data/keystores/teku-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.txt b/tests/data/keystores/teku-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.txt new file mode 100644 index 00000000..b2ce4dfd --- /dev/null +++ b/tests/data/keystores/teku-secrets/0xb3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9.txt @@ -0,0 +1 @@ +BWBoV1UZpkO4cUA-t8T9aViJ0sBfilR7qJFHgU4tBSc= \ No newline at end of file