diff --git a/node/libs/crypto/src/ed25519/mod.rs b/node/libs/crypto/src/ed25519/mod.rs index f8340524..b6fdf8eb 100644 --- a/node/libs/crypto/src/ed25519/mod.rs +++ b/node/libs/crypto/src/ed25519/mod.rs @@ -97,6 +97,12 @@ impl Ord for PublicKey { } } +impl PartialEq for SecretKey { + fn eq(&self, other: &Self) -> bool { + self.public() == other.public() + } +} + /// ed25519 signature. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Signature(ed::Signature); diff --git a/node/libs/roles/src/node/keys.rs b/node/libs/roles/src/node/keys.rs index 454ca2d3..b3fb0441 100644 --- a/node/libs/roles/src/node/keys.rs +++ b/node/libs/roles/src/node/keys.rs @@ -9,7 +9,7 @@ use zksync_consensus_crypto::{ed25519, ByteFmt, Text, TextFmt}; use zksync_consensus_utils::enum_util::Variant; /// A node's secret key. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct SecretKey(pub(super) Arc); impl SecretKey { diff --git a/node/libs/roles/src/validator/keys/secret_key.rs b/node/libs/roles/src/validator/keys/secret_key.rs index 644bde2a..82895393 100644 --- a/node/libs/roles/src/validator/keys/secret_key.rs +++ b/node/libs/roles/src/validator/keys/secret_key.rs @@ -7,7 +7,7 @@ use zksync_consensus_utils::enum_util::Variant; /// A secret key for the validator role. /// SecretKey is put into an Arc, so that we can clone it, /// without copying the secret all over the RAM. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct SecretKey(pub(crate) Arc); impl SecretKey { diff --git a/node/tools/src/bin/deployer.rs b/node/tools/src/bin/deployer.rs index f63cac84..49bea272 100644 --- a/node/tools/src/bin/deployer.rs +++ b/node/tools/src/bin/deployer.rs @@ -36,24 +36,22 @@ fn generate_consensus_nodes(nodes: usize, seed_nodes_amount: Option) -> V let node_keys: Vec = (0..nodes).map(|_| SecretKey::generate()).collect(); - let default_config = AppConfig { - server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), NODES_PORT), - public_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), NODES_PORT).into(), - debug_addr: None, - metrics_server_addr: None, - genesis: setup.genesis.clone(), - max_payload_size: 1000000, - gossip_dynamic_inbound_limit: 2, - gossip_static_inbound: [].into(), - gossip_static_outbound: [].into(), - }; - let mut cfgs: Vec = (0..nodes) .map(|i| ConsensusNode { id: format!("consensus-node-{i:0>2}"), - config: default_config.clone(), - key: node_keys[i].clone(), - validator_key: Some(validator_keys[i].clone()), + config: AppConfig { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), NODES_PORT), + public_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), NODES_PORT).into(), + debug_addr: None, + metrics_server_addr: None, + genesis: setup.genesis.clone(), + max_payload_size: 1000000, + validator_key: Some(validator_keys[i].clone()), + node_key: node_keys[i].clone(), + gossip_dynamic_inbound_limit: 2, + gossip_static_inbound: [].into(), + gossip_static_outbound: [].into(), + }, node_addr: None, //It's not assigned yet is_seed: i < seed_nodes_amount, }) diff --git a/node/tools/src/bin/localnet_config.rs b/node/tools/src/bin/localnet_config.rs index 6d548648..c148c18a 100644 --- a/node/tools/src/bin/localnet_config.rs +++ b/node/tools/src/bin/localnet_config.rs @@ -66,6 +66,8 @@ fn main() -> anyhow::Result<()> { .map(|port| SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port)), genesis: setup.genesis.clone(), max_payload_size: 1000000, + node_key: node_keys[i].clone(), + validator_key: Some(validator_keys[i].clone()), gossip_dynamic_inbound_limit: 0, gossip_static_inbound: HashSet::default(), gossip_static_outbound: HashMap::default(), diff --git a/node/tools/src/config.rs b/node/tools/src/config.rs index b2c91f8c..a6999f9f 100644 --- a/node/tools/src/config.rs +++ b/node/tools/src/config.rs @@ -4,9 +4,8 @@ use anyhow::Context as _; use serde_json::{ser::Formatter, Serializer}; use std::{ collections::{HashMap, HashSet}, - fs, net::SocketAddr, - path::{Path, PathBuf}, + path::PathBuf, }; use zksync_concurrency::{ctx, net}; use zksync_consensus_bft as bft; @@ -14,7 +13,23 @@ use zksync_consensus_crypto::{read_optional_text, read_required_text, Text, Text use zksync_consensus_executor as executor; use zksync_consensus_roles::{node, validator}; use zksync_consensus_storage::{BlockStore, BlockStoreRunner}; -use zksync_protobuf::{read_required, required, serde::Serde, ProtoFmt}; +use zksync_protobuf::{read_required, required, ProtoFmt}; + +fn read_required_secret_text(text: &Option) -> anyhow::Result { + Text::new( + text.as_ref() + .ok_or_else(|| anyhow::format_err!("missing"))?, + ) + .decode() + .map_err(|_| anyhow::format_err!("invalid format")) +} + +fn read_optional_secret_text(text: &Option) -> anyhow::Result> { + text.as_ref() + .map(|t| Text::new(t).decode()) + .transpose() + .map_err(|_| anyhow::format_err!("invalid format")) +} /// Ports for the nodes to listen on kubernetes pod. pub const NODES_PORT: u16 = 3054; @@ -77,7 +92,9 @@ pub struct AppConfig { pub genesis: validator::Genesis, pub max_payload_size: usize, + pub validator_key: Option, + pub node_key: node::SecretKey, pub gossip_dynamic_inbound_limit: usize, pub gossip_static_inbound: HashSet, pub gossip_static_outbound: HashMap, @@ -113,7 +130,11 @@ impl ProtoFmt for AppConfig { max_payload_size: required(&r.max_payload_size) .and_then(|x| Ok((*x).try_into()?)) .context("max_payload_size")?, + // TODO: read secret. + validator_key: read_optional_secret_text(&r.validator_secret_key) + .context("validator_secret_key")?, + node_key: read_required_secret_text(&r.node_secret_key).context("node_secret_key")?, gossip_dynamic_inbound_limit: required(&r.gossip_dynamic_inbound_limit) .and_then(|x| Ok((*x).try_into()?)) .context("gossip_dynamic_inbound_limit")?, @@ -131,7 +152,9 @@ impl ProtoFmt for AppConfig { genesis: Some(self.genesis.build()), max_payload_size: Some(self.max_payload_size.try_into().unwrap()), + validator_secret_key: self.validator_key.as_ref().map(TextFmt::encode), + node_secret_key: Some(self.node_key.encode()), gossip_dynamic_inbound_limit: Some( self.gossip_dynamic_inbound_limit.try_into().unwrap(), ), @@ -152,86 +175,12 @@ impl ProtoFmt for AppConfig { } } -/// Configuration information. #[derive(Debug)] -pub struct ConfigArgs<'a> { - /// Node configuration. - pub config_args: ConfigSource<'a>, - /// Path to the rocksdb database. - pub database: &'a Path, -} - -#[derive(Debug)] -pub enum ConfigSource<'a> { - CliConfig { - /// Node configuration from command line. - config: AppConfig, - /// Node key as a string. - node_key: node::SecretKey, - /// Validator key as a string. - validator_key: Option, - }, - PathConfig { - /// Path to a JSON file with node configuration. - config_file: &'a Path, - /// Path to a validator key file. - validator_key_file: &'a Path, - /// Path to a node key file. - node_key_file: &'a Path, - }, -} - pub struct Configs { pub app: AppConfig, - pub validator_key: Option, - pub node_key: node::SecretKey, pub database: PathBuf, } -impl<'a> ConfigArgs<'a> { - // Loads configs from the file system. - pub fn load(self) -> anyhow::Result { - match self.config_args { - ConfigSource::CliConfig { - config, - node_key, - validator_key, - } => Ok(Configs { - app: config.clone(), - validator_key: validator_key.clone(), - node_key: node_key.clone(), - database: self.database.into(), - }), - ConfigSource::PathConfig { - config_file, - validator_key_file, - node_key_file, - } => Ok(Configs { - app: (|| { - let app = fs::read_to_string(config_file).context("failed reading file")?; - decode_json::>(&app).context("failed decoding JSON") - })() - .with_context(|| config_file.display().to_string())? - .0, - - validator_key: fs::read_to_string(validator_key_file) - .ok() - .map(|value| Text::new(&value).decode().context("failed decoding key")) - .transpose() - .with_context(|| validator_key_file.display().to_string())?, - - node_key: (|| { - let key = fs::read_to_string(node_key_file).context("failed reading file")?; - Text::new(&key).decode().context("failed decoding key") - })() - .with_context(|| node_key_file.display().to_string())?, - - database: self.database.into(), - }), - } - } -} - impl Configs { pub async fn make_executor( &self, @@ -243,18 +192,24 @@ impl Configs { config: executor::Config { server_addr: self.app.server_addr, public_addr: self.app.public_addr.clone(), - node_key: self.node_key.clone(), + node_key: self.app.node_key.clone(), gossip_dynamic_inbound_limit: self.app.gossip_dynamic_inbound_limit, gossip_static_inbound: self.app.gossip_static_inbound.clone(), gossip_static_outbound: self.app.gossip_static_outbound.clone(), max_payload_size: self.app.max_payload_size, }, block_store, - validator: self.validator_key.as_ref().map(|key| executor::Validator { - key: key.clone(), - replica_store: Box::new(store), - payload_manager: Box::new(bft::testonly::RandomPayload(self.app.max_payload_size)), - }), + validator: self + .app + .validator_key + .as_ref() + .map(|key| executor::Validator { + key: key.clone(), + replica_store: Box::new(store), + payload_manager: Box::new(bft::testonly::RandomPayload( + self.app.max_payload_size, + )), + }), }; Ok((e, runner)) } diff --git a/node/tools/src/k8s.rs b/node/tools/src/k8s.rs index 40c1936f..6ad0a6d7 100644 --- a/node/tools/src/k8s.rs +++ b/node/tools/src/k8s.rs @@ -18,8 +18,6 @@ use kube::{ use std::{collections::BTreeMap, net::SocketAddr, time::Duration}; use tokio::time; use tracing::log::info; -use zksync_consensus_crypto::TextFmt; -use zksync_consensus_roles::{node, validator}; use zksync_protobuf::serde::Serde; /// Docker image name for consensus nodes. @@ -35,10 +33,6 @@ pub struct ConsensusNode { pub id: String, /// Node configuration pub config: AppConfig, - /// Node key - pub key: node::SecretKey, - /// Node key - pub validator_key: Option, /// Full NodeAddr pub node_addr: Option, /// Is seed node (meaning it has no gossipStaticOutbound configuration) @@ -74,7 +68,7 @@ impl ConsensusNode { .pod_ip .context("Pod IP address not present")?; self.node_addr = Some(NodeAddr { - key: self.key.public(), + key: self.config.node_key.public(), addr: SocketAddr::new(ip.parse()?, config::NODES_PORT).into(), }); Ok(()) @@ -339,20 +333,13 @@ fn is_pod_running(pod: &Pod) -> bool { } fn get_cli_args(consensus_node: &ConsensusNode) -> Vec { - let mut cli_args = [ + vec![ "--config".to_string(), config::encode_with_serializer( &Serde(consensus_node.config.clone()), serde_json::Serializer::new(vec![]), ), - "--node-key".to_string(), - TextFmt::encode(&consensus_node.key), ] - .to_vec(); - if let Some(key) = &consensus_node.validator_key { - cli_args.append(&mut ["--validator-key".to_string(), TextFmt::encode(key)].to_vec()) - }; - cli_args } async fn retry(retries: usize, delay: Duration, mut f: F) -> anyhow::Result diff --git a/node/tools/src/lib.rs b/node/tools/src/lib.rs index 411d143a..48258164 100644 --- a/node/tools/src/lib.rs +++ b/node/tools/src/lib.rs @@ -9,7 +9,5 @@ mod store; #[cfg(test)] mod tests; -pub use config::{ - decode_json, encode_json, AppConfig, ConfigArgs, ConfigSource, NodeAddr, NODES_PORT, -}; +pub use config::{decode_json, encode_json, AppConfig, Configs, NodeAddr, NODES_PORT}; pub use rpc::server::RPCServer; diff --git a/node/tools/src/main.rs b/node/tools/src/main.rs index 88659400..6553b905 100644 --- a/node/tools/src/main.rs +++ b/node/tools/src/main.rs @@ -7,84 +7,34 @@ use tracing::metadata::LevelFilter; use tracing_subscriber::{prelude::*, Registry}; use vise_exporter::MetricsExporter; use zksync_concurrency::{ctx, scope}; -use zksync_consensus_crypto::{Text, TextFmt}; -use zksync_consensus_roles::{node, validator}; -use zksync_consensus_tools::{ - decode_json, AppConfig, ConfigArgs, ConfigSource, RPCServer, NODES_PORT, -}; +use zksync_consensus_tools::{decode_json, AppConfig, Configs, RPCServer, NODES_PORT}; use zksync_protobuf::serde::Serde; /// Command-line application launching a node executor. #[derive(Debug, Parser)] struct Cli { - /// Full json config - #[arg(long, - value_parser(parse_config), - requires="node_key", - conflicts_with_all=["config_file", "validator_key_file", "node_key_file"])] - config: Option>, - /// Plain node key - #[arg(long, - value_parser(parse_key::), - requires="config", - conflicts_with_all=["config_file", "validator_key_file", "node_key_file"])] - node_key: Option, - /// Plain validator key - #[arg(long, - value_parser(parse_key::), - requires_all=["config", "node_key"], - conflicts_with_all=["config_file", "validator_key_file", "node_key_file"])] - validator_key: Option, - /// Path to a validator key file. If set to an empty string, validator key will not be read - /// (i.e., a node will be initialized as a non-validator node). - #[arg(long, - default_value = "./validator_key", - conflicts_with_all=["config", "validator_key", "node_key"])] - validator_key_file: PathBuf, - /// Path to a JSON file with node configuration. - #[arg(long, - default_value = "./config.json", - conflicts_with_all=["config", "validator_key", "node_key"])] - config_file: PathBuf, - /// Path to a node key file. - #[arg(long, - default_value = "./node_key", - conflicts_with_all=["config", "validator_key", "node_key"])] - node_key_file: PathBuf, + /// Path to the file with json config. + #[arg(long, default_value = "./config.json")] + config_path: PathBuf, + /// Inlined json config. + #[arg(long, conflicts_with = "config_path")] + config: Option, /// Path to the rocksdb database of the node. #[arg(long, default_value = "./database")] database: PathBuf, } -/// Function to let clap parse the command line `config` argument -fn parse_config(val: &str) -> anyhow::Result> { - decode_json(val) -} - -/// Node/validator key parser for clap -fn parse_key(val: &str) -> anyhow::Result { - Text::new(val).decode().context("failed decoding key") -} - impl Cli { - /// Extracts configuration paths from these args. - fn config_args(&self) -> ConfigArgs<'_> { - let config_args = match &self.config { - Some(config) => ConfigSource::CliConfig { - config: config.clone().0, - node_key: self.node_key.clone().unwrap(), // node_key is present as it is enforced by clap rules - validator_key: self.validator_key.clone(), - }, - None => ConfigSource::PathConfig { - config_file: &self.config_file, - validator_key_file: &self.validator_key_file, - node_key_file: &self.node_key_file, - }, + /// Extracts configuration from the cli args. + fn load(&self) -> anyhow::Result { + let raw = match &self.config { + Some(raw) => raw.clone(), + None => fs::read_to_string(&self.config_path)?, }; - ConfigArgs { - config_args, - database: &self.database, - } + Ok(Configs { + app: decode_json::>(&raw)?.0, + database: self.database.clone(), + }) } } @@ -99,8 +49,8 @@ fn check_public_addr(cfg: &mut AppConfig) -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { let args: Cli = Cli::parse(); - tracing::trace!(?args, "Starting node"); let ctx = &ctx::root(); + tracing::trace!("Starting node"); // Create log file. fs::create_dir_all("logs/")?; @@ -134,7 +84,7 @@ async fn main() -> anyhow::Result<()> { // Load the config files. tracing::debug!("Loading config files."); - let mut configs = args.config_args().load().context("config_args().load()")?; + let mut configs = args.load().context("config_args().load()")?; // if `PUBLIC_ADDR` env var is set, use it to override publicAddr in config check_public_addr(&mut configs.app).context("check_public_addr()")?; diff --git a/node/tools/src/proto/mod.proto b/node/tools/src/proto/mod.proto index 0650b21c..449d144f 100644 --- a/node/tools/src/proto/mod.proto +++ b/node/tools/src/proto/mod.proto @@ -27,6 +27,15 @@ // NodePublicKey - public key of the node (gossip network participant) of the form "node:public::" // Currently only ed25519 signature scheme is supported for nodes. // example: "node:public:ed25519:d36607699a0a3fbe3de16947928cf299484219ff62ca20f387795b0859dbe501" +// +// Since this application is just a (testonly) example of how to use consensus, +// the security is not that important, so for simplicity we embed the secrets into the config file: +// +// ValidatorSecretKey - secret key of the validator (consensus participant) of the form "validator:secret::" +// Currently only bn254 signature scheme is supported for validators. +// +// NodeSecretKey - secret key of the node (gossip network participant) of the form "node:secret::" +// Currently only ed25519 signature scheme is supported for nodes. syntax = "proto3"; package zksync.tools; @@ -67,7 +76,13 @@ message AppConfig { // Maximal size of the block payload. optional uint64 max_payload_size = 5; // required; bytes + // Validator secret key. + optional string validator_secret_key = 10; // optional; ValidatorSecretKey + // Gossip network + + // Node secret key. + optional string node_secret_key = 11; // required; NodeSecretKey // Limit on the number of gossip network inbound connections outside // of the `gossip_static_inbound` set. diff --git a/node/tools/src/tests.rs b/node/tools/src/tests.rs index c235ed61..f47a9994 100644 --- a/node/tools/src/tests.rs +++ b/node/tools/src/tests.rs @@ -16,14 +16,16 @@ impl Distribution for EncodeDist { metrics_server_addr: self.sample(rng), genesis: rng.gen(), + max_payload_size: rng.gen(), + validator_key: self.sample_opt(|| rng.gen()), + node_key: rng.gen(), gossip_dynamic_inbound_limit: rng.gen(), gossip_static_inbound: self.sample_range(rng).map(|_| rng.gen()).collect(), gossip_static_outbound: self .sample_range(rng) .map(|_| (rng.gen(), self.sample(rng))) .collect(), - max_payload_size: rng.gen(), } } }