From 3194e0fff421ebd9494adb08894db1be1085183a Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 17 Jul 2026 11:14:34 -0500 Subject: [PATCH] feat: wire sequencers individual secp256k1 key into manifest. --- Cargo.lock | 1 + crates/node/src/cli.rs | 34 ++++++++++- crates/node/tests/it/utils.rs | 40 ++++++++++--- crates/p2p/Cargo.toml | 1 + crates/p2p/README.md | 15 ++--- crates/p2p/src/identity.rs | 68 ++++++++++++++++++--- crates/p2p/src/manifest.rs | 110 +++++++++++++++++++++++++++++----- crates/p2p/src/runtime.rs | 49 ++++++++++++--- 8 files changed, 270 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ea6f7cbc..b8623dde7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13550,6 +13550,7 @@ name = "zone-p2p" version = "0.1.0" dependencies = [ "alloy-primitives", + "alloy-signer-local", "commonware-codec", "commonware-cryptography", "commonware-p2p", diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index eb1c805ae..3ac4eaf41 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -107,9 +107,13 @@ fn run_node(mut cli: Cli) -> eyre::Result<()> { let ed25519_key_path = args.p2p_key.as_ref().ok_or_else(|| { eyre::eyre!("--p2p.key is required with --sequencer.manifest") })?; + let secp256k1_key_path = args.secp256k1_key.as_ref().ok_or_else(|| { + eyre::eyre!("--secp256k1.key is required with --sequencer.manifest") + })?; P2pConfig::load( manifest_path, ed25519_key_path, + secp256k1_key_path, args.p2p_listen, args.p2p_bypass_ip_check, args.zone_id, @@ -123,6 +127,7 @@ fn run_node(mut cli: Cli) -> eyre::Result<()> { target: "reth::cli", role = %config.role(), ed25519_public_key = %config.ed25519_public_key(), + secp256k1_address = %config.secp256k1_address(), listen = %config.listen(), "Validated multi-sequencer manifest and local identity" ); @@ -218,7 +223,7 @@ pub struct ZoneArgs { long = "sequencer.manifest", env = "SEQUENCER_MANIFEST", value_name = "PATH", - requires = "p2p_key", + requires_all = ["p2p_key", "secp256k1_key"], conflicts_with = "enable_sequencer" )] pub sequencer_manifest: Option, @@ -232,6 +237,15 @@ pub struct ZoneArgs { )] pub p2p_key: Option, + /// Path to this node's hex-encoded individual secp256k1 private key. + #[arg( + long = "secp256k1.key", + env = "SECP256K1_KEY", + value_name = "PATH", + requires = "sequencer_manifest" + )] + pub secp256k1_key: Option, + /// Socket address bound for multi-sequencer Commonware traffic. #[arg( long = "p2p.listen", @@ -394,7 +408,7 @@ mod tests { } #[test] - fn manifest_mode_requires_a_p2p_key_and_conflicts_with_legacy_sequencer() { + fn manifest_mode_requires_node_keys_and_conflicts_with_legacy_sequencer() { let common = [ "tempo-zone", "--l1.rpc-url", @@ -416,11 +430,25 @@ mod tests { clap::error::ErrorKind::MissingRequiredArgument ); + let missing_secp256k1_key = ZoneArgsParser::try_parse_from(common.into_iter().chain([ + "--sequencer.manifest", + "zone.toml", + "--p2p.key", + "node.key", + ])) + .unwrap_err(); + assert_eq!( + missing_secp256k1_key.kind(), + clap::error::ErrorKind::MissingRequiredArgument + ); + let conflict = ZoneArgsParser::try_parse_from(common.into_iter().chain([ "--sequencer.manifest", "zone.toml", "--p2p.key", "node.key", + "--secp256k1.key", + "node-secp256k1.key", "--sequencer", ])) .unwrap_err(); @@ -472,6 +500,8 @@ mod tests { "zone.toml", "--p2p.key", "node.key", + "--secp256k1.key", + "node-secp256k1.key", "--p2p.bypass-ip-check", ])) .unwrap(); diff --git a/crates/node/tests/it/utils.rs b/crates/node/tests/it/utils.rs index ccdc2e70b..2111d1608 100644 --- a/crates/node/tests/it/utils.rs +++ b/crates/node/tests/it/utils.rs @@ -5,7 +5,7 @@ use alloy_primitives::{Address, B256, U256, address, keccak256}; use alloy_provider::{DynProvider, Provider, ProviderBuilder}; use alloy_rlp::Encodable; use alloy_rpc_types_eth::{BlockNumberOrTag, Filter}; -use alloy_signer_local::{MnemonicBuilder, coins_bip39::English}; +use alloy_signer_local::{MnemonicBuilder, PrivateKeySigner, coins_bip39::English}; use alloy_sol_types::{SolEvent, SolValue}; use commonware_codec::Encode as _; use commonware_cryptography::{Signer as _, ed25519::PrivateKey as Ed25519PrivateKey}; @@ -2645,6 +2645,10 @@ pub(crate) async fn start_local_p2p_pair( Ed25519PrivateKey::from_seed(103), ]; let public_keys = identities.each_ref().map(|key| key.public_key()); + let secp256k1_keys = [101_u64, 102, 103].map(|key| format!("0x{key:064x}")); + let secp256k1_signers = secp256k1_keys + .each_ref() + .map(|key| key.parse::().unwrap()); let unique = NEXT_CHAIN_ID.fetch_add(1, Ordering::Relaxed); let config_dir = std::env::temp_dir().join(format!( @@ -2657,10 +2661,16 @@ pub(crate) async fn start_local_p2p_pair( "zone_id = 0\nleader_ed25519_public_key = \"{}\"\n", const_hex::encode_prefixed(public_keys[0].as_ref()) ); - for (index, (public_key, address)) in public_keys.iter().zip(addresses).enumerate() { + for (index, ((public_key, secp256k1_signer), address)) in public_keys + .iter() + .zip(&secp256k1_signers) + .zip(addresses) + .enumerate() + { manifest.push_str(&format!( - "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\naddress = \"{address}\"\n", - const_hex::encode_prefixed(public_key.as_ref()) + "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\nsecp256k1_address = \"{}\"\naddress = \"{address}\"\n", + const_hex::encode_prefixed(public_key.as_ref()), + secp256k1_signer.address(), )); } std::fs::write(&manifest_path, manifest)?; @@ -2671,9 +2681,12 @@ pub(crate) async fn start_local_p2p_pair( &key_path, const_hex::encode_prefixed(identities[index].encode().as_ref()), )?; + let secp256k1_key_path = config_dir.join(format!("node-{index}-secp256k1.key")); + std::fs::write(&secp256k1_key_path, &secp256k1_keys[index])?; configs.push(P2pConfig::load( &manifest_path, &key_path, + &secp256k1_key_path, addresses[index], false, 0, @@ -2737,6 +2750,10 @@ pub(crate) fn leader_p2p_config(listen: SocketAddr) -> eyre::Result { Ed25519PrivateKey::from_seed(203), ]; let public_keys = identities.each_ref().map(|key| key.public_key()); + let secp256k1_keys = [201_u64, 202, 203].map(|key| format!("0x{key:064x}")); + let secp256k1_signers = secp256k1_keys + .each_ref() + .map(|key| key.parse::().unwrap()); let addresses = [listen, available_address()?, available_address()?]; let config_dir = std::env::temp_dir().join(format!( "tempo-zone-p2p-config-{}-{}", @@ -2750,10 +2767,16 @@ pub(crate) fn leader_p2p_config(listen: SocketAddr) -> eyre::Result { "zone_id = 0\nleader_ed25519_public_key = \"{}\"\n", const_hex::encode_prefixed(public_keys[0].as_ref()) ); - for (index, (public_key, address)) in public_keys.iter().zip(addresses).enumerate() { + for (index, ((public_key, secp256k1_signer), address)) in public_keys + .iter() + .zip(&secp256k1_signers) + .zip(addresses) + .enumerate() + { manifest.push_str(&format!( - "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\naddress = \"{address}\"\n", - const_hex::encode_prefixed(public_key.as_ref()) + "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\nsecp256k1_address = \"{}\"\naddress = \"{address}\"\n", + const_hex::encode_prefixed(public_key.as_ref()), + secp256k1_signer.address(), )); } std::fs::write(&manifest_path, manifest)?; @@ -2761,9 +2784,12 @@ pub(crate) fn leader_p2p_config(listen: SocketAddr) -> eyre::Result { &key_path, const_hex::encode_prefixed(identities[0].encode().as_ref()), )?; + let secp256k1_key_path = config_dir.join("leader-secp256k1.key"); + std::fs::write(&secp256k1_key_path, &secp256k1_keys[0])?; let config = P2pConfig::load( &manifest_path, &key_path, + &secp256k1_key_path, listen, false, 0, diff --git a/crates/p2p/Cargo.toml b/crates/p2p/Cargo.toml index 058bf4c6c..f239bb1ff 100644 --- a/crates/p2p/Cargo.toml +++ b/crates/p2p/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] alloy-primitives.workspace = true +alloy-signer-local.workspace = true commonware-codec.workspace = true commonware-cryptography.workspace = true diff --git a/crates/p2p/README.md b/crates/p2p/README.md index 3da3ee4e6..d416466f7 100644 --- a/crates/p2p/README.md +++ b/crates/p2p/README.md @@ -47,31 +47,30 @@ leader_ed25519_public_key = "0xleader..." [[nodes]] name = "leader" ed25519_public_key = "0xleader..." +secp256k1_address = "0x1111111111111111111111111111111111111111" address = "leader.zone.internal:9200" -# Future on-chain quorum identity; not accepted by the schema yet: -# secp256k1_address = "0x1111111111111111111111111111111111111111" [[nodes]] name = "follower-a" ed25519_public_key = "0xfa..." +secp256k1_address = "0x2222222222222222222222222222222222222222" address = "follower-a.zone.internal:9200" -# secp256k1_address = "0x2222222222222222222222222222222222222222" [[nodes]] name = "follower-b" ed25519_public_key = "0xfb..." +secp256k1_address = "0x3333333333333333333333333333333333333333" address = "follower-b.zone.internal:9200" -# secp256k1_address = "0x3333333333333333333333333333333333333333" ``` The manifest loader validates that: - there are at least three nodes; -- node names and Ed25519 public keys are unique; +- node names, Ed25519 public keys, and secp256k1 addresses are unique; - every address has a non-zero port; - `leader_ed25519_public_key` identifies one of the nodes; - the manifest's `zone_id` matches `--zone.id`; and -- the local private key corresponds to a manifest member. +- both local private keys correspond to the same manifest member. ## Generate a Commonware identity @@ -91,11 +90,13 @@ Add these arguments to the node's normal command: ```text --sequencer.manifest ./zone-manifest.toml --p2p.key ./leader-p2p.key +--secp256k1.key ./leader-secp256k1.key --p2p.listen 0.0.0.0:9200 --sequencer.role leader ``` -Use each node's own key file and listener address. +Use each node's own key files and listener address. The secp256k1 key is loaded +and validated now but will only be used once zone-block quorum signing is wired. The `--sequencer` flag conflicts with `--sequencer.manifest` because the manifest determines whether the node starts the sequencer tasks. diff --git a/crates/p2p/src/identity.rs b/crates/p2p/src/identity.rs index 4ce342d44..6bb8e84d6 100644 --- a/crates/p2p/src/identity.rs +++ b/crates/p2p/src/identity.rs @@ -1,5 +1,7 @@ -use std::{fmt, path::Path}; +use std::path::Path; +use alloy_primitives::Address; +use alloy_signer_local::PrivateKeySigner; use commonware_codec::DecodeExt as _; use commonware_cryptography::{ Signer as _, @@ -7,7 +9,7 @@ use commonware_cryptography::{ }; /// An Ed25519 private key loaded as a node's Commonware identity. -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct Ed25519Identity(PrivateKey); impl Ed25519Identity { @@ -39,11 +41,33 @@ impl Ed25519Identity { } } -impl fmt::Debug for Ed25519Identity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Ed25519Identity") - .field("ed25519_public_key", &self.ed25519_public_key()) - .finish_non_exhaustive() +/// A node's individual secp256k1 key. +#[derive(Clone, Debug)] +pub(crate) struct Secp256k1Identity(PrivateKeySigner); + +impl Secp256k1Identity { + /// Reads an unencrypted, hex-encoded private key from `path`. + pub(crate) fn read_from_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + let encoded = + std::fs::read_to_string(path).map_err(|source| Secp256k1IdentityError::Read { + path: path.to_owned(), + source, + })?; + Self::from_hex(encoded.trim()) + } + + /// Parses an unencrypted, hex-encoded secp256k1 private key. + pub(crate) fn from_hex(encoded: &str) -> Result { + encoded + .parse::() + .map(Self) + .map_err(|source| Secp256k1IdentityError::Invalid(source.to_string())) + } + + /// Returns the Ethereum address corresponding to this private key. + pub(crate) fn address(&self) -> Address { + self.0.address() } } @@ -64,12 +88,26 @@ pub(crate) enum Ed25519IdentityError { Decode(#[source] commonware_codec::Error), } +/// Errors produced while loading a node's individual secp256k1 identity. +#[derive(Debug, thiserror::Error)] +pub(crate) enum Secp256k1IdentityError { + #[error("failed reading secp256k1 identity `{path}`")] + Read { + path: std::path::PathBuf, + #[source] + source: std::io::Error, + }, + + #[error("invalid secp256k1 private key: {0}")] + Invalid(String), +} + #[cfg(test)] mod tests { use commonware_codec::Encode as _; use commonware_cryptography::{Signer as _, ed25519::PrivateKey}; - use super::Ed25519Identity; + use super::{Ed25519Identity, Secp256k1Identity}; #[test] fn parses_hex_identity() { @@ -79,4 +117,18 @@ mod tests { assert_eq!(identity.ed25519_public_key(), key.public_key()); } + + #[test] + fn parses_secp256k1_identity_without_exposing_the_key() { + let identity = Secp256k1Identity::from_hex( + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(); + + assert_eq!( + identity.address().to_string(), + "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" + ); + assert!(!format!("{identity:?}").contains("0000000000000001")); + } } diff --git a/crates/p2p/src/manifest.rs b/crates/p2p/src/manifest.rs index 1229aa71a..666133e0e 100644 --- a/crates/p2p/src/manifest.rs +++ b/crates/p2p/src/manifest.rs @@ -5,6 +5,7 @@ use std::{ path::Path, }; +use alloy_primitives::Address as EthereumAddress; use commonware_codec::DecodeExt as _; use commonware_cryptography::ed25519::PublicKey; use commonware_p2p::{Address, Ingress}; @@ -96,9 +97,7 @@ impl std::str::FromStr for ManifestAddress { pub struct ManifestNode { name: String, ed25519_public_key: PublicKey, - // TODO: Add `secp256k1_address`, from this - // node's individual L1/attestation key. It is distinct from the Commonware - // Ed25519 identity above and will be used for the on-chain quorum. + secp256k1_address: EthereumAddress, address: ManifestAddress, } @@ -113,6 +112,11 @@ impl ManifestNode { &self.ed25519_public_key } + /// Address derived from this node's individual secp256k1 key. + pub const fn secp256k1_address(&self) -> EthereumAddress { + self.secp256k1_address + } + /// Node's advertised P2P address. pub const fn address(&self) -> &ManifestAddress { &self.address @@ -139,6 +143,7 @@ impl ZoneManifest { parse_ed25519_public_key("leader_ed25519_public_key", &raw.leader_ed25519_public_key)?; let mut names = BTreeSet::new(); let mut ed25519_public_keys = BTreeSet::new(); + let mut secp256k1_addresses = BTreeSet::new(); let mut nodes = Vec::with_capacity(raw.nodes.len()); for raw_node in raw.nodes { @@ -159,6 +164,18 @@ impl ZoneManifest { )); } + let secp256k1_address = raw_node + .secp256k1_address + .parse::() + .map_err(|source| ManifestError::InvalidSecp256k1Address { + node: raw_node.name.clone(), + address: raw_node.secp256k1_address.clone(), + reason: source.to_string(), + })?; + if !secp256k1_addresses.insert(secp256k1_address) { + return Err(ManifestError::DuplicateSecp256k1Address(secp256k1_address)); + } + let address = raw_node .address @@ -171,6 +188,7 @@ impl ZoneManifest { nodes.push(ManifestNode { name: raw_node.name, ed25519_public_key, + secp256k1_address, address, }); } @@ -203,6 +221,7 @@ impl ZoneManifest { &self, expected_zone_id: u32, local_ed25519_public_key: &PublicKey, + local_secp256k1_address: EthereumAddress, asserted_role: Option, ) -> Result { if self.zone_id != expected_zone_id { @@ -212,9 +231,22 @@ impl ZoneManifest { }); } - let role = self.role_of(local_ed25519_public_key).ok_or_else(|| { - ManifestError::LocalNodeNotFound(local_ed25519_public_key.to_string()) - })?; + let local_node = self + .node_by_ed25519_public_key(local_ed25519_public_key) + .ok_or_else(|| { + ManifestError::LocalNodeNotFound(local_ed25519_public_key.to_string()) + })?; + if local_node.secp256k1_address != local_secp256k1_address { + return Err(ManifestError::LocalSecp256k1AddressMismatch { + manifest: local_node.secp256k1_address, + local: local_secp256k1_address, + }); + } + let role = if local_ed25519_public_key == &self.leader_ed25519_public_key { + Role::Leader + } else { + Role::Follower + }; if let Some(asserted) = asserted_role && asserted != role { @@ -243,9 +275,8 @@ impl ZoneManifest { /// Returns the role of a manifest member, or `None` for an unknown Ed25519 key. pub fn role_of(&self, ed25519_public_key: &PublicKey) -> Option { - self.nodes - .iter() - .any(|node| node.ed25519_public_key() == ed25519_public_key) + self.node_by_ed25519_public_key(ed25519_public_key) + .is_some() .then_some(if ed25519_public_key == &self.leader_ed25519_public_key { Role::Leader } else { @@ -253,6 +284,12 @@ impl ZoneManifest { }) } + fn node_by_ed25519_public_key(&self, ed25519_public_key: &PublicKey) -> Option<&ManifestNode> { + self.nodes + .iter() + .find(|node| node.ed25519_public_key() == ed25519_public_key) + } + pub(crate) fn has_dns_addresses(&self) -> bool { self.nodes.iter().any(|node| node.address.is_dns()) } @@ -271,6 +308,7 @@ struct RawManifest { struct RawManifestNode { name: String, ed25519_public_key: String, + secp256k1_address: String, address: String, } @@ -312,9 +350,19 @@ pub enum ManifestError { #[error("duplicate sequencer manifest Ed25519 public key `{0}`")] DuplicateEd25519PublicKey(String), + #[error("duplicate sequencer manifest secp256k1 address `{0}`")] + DuplicateSecp256k1Address(EthereumAddress), + #[error("invalid Ed25519 public key in `{field}`: {reason}")] InvalidEd25519PublicKey { field: String, reason: String }, + #[error("invalid secp256k1 address `{address}` for node `{node}`: {reason}")] + InvalidSecp256k1Address { + node: String, + address: String, + reason: String, + }, + #[error("invalid address `{address}` for node `{node}`: {reason}")] InvalidAddress { node: String, @@ -331,6 +379,14 @@ pub enum ManifestError { #[error("this node's Ed25519 public key `{0}` is not present in the sequencer manifest")] LocalNodeNotFound(String), + #[error( + "this node's secp256k1 address `{local}` does not match its manifest address `{manifest}`" + )] + LocalSecp256k1AddressMismatch { + manifest: EthereumAddress, + local: EthereumAddress, + }, + #[error("--sequencer.role asserts `{asserted}`, but the manifest assigns `{manifest}`")] RoleMismatch { asserted: Role, manifest: Role }, } @@ -346,6 +402,10 @@ mod tests { const_hex::encode_prefixed(key.as_ref()) } + fn secp256k1_address(seed: u64) -> String { + format!("0x{seed:040x}") + } + fn manifest(leader: u64, nodes: &[(u64, &str, &str)]) -> String { let mut value = format!( "zone_id = 7\nleader_ed25519_public_key = \"{}\"\n", @@ -353,8 +413,9 @@ mod tests { ); for (key, name, address) in nodes { value.push_str(&format!( - "\n[[nodes]]\nname = \"{name}\"\ned25519_public_key = \"{}\"\naddress = \"{address}\"\n", - ed25519_public_key(*key) + "\n[[nodes]]\nname = \"{name}\"\ned25519_public_key = \"{}\"\nsecp256k1_address = \"{}\"\naddress = \"{address}\"\n", + ed25519_public_key(*key), + secp256k1_address(*key), )); } value @@ -375,12 +436,19 @@ mod tests { let follower = PrivateKey::from_seed(2).public_key(); assert_eq!( - manifest.validate_node(7, &leader, None).unwrap(), + manifest + .validate_node(7, &leader, secp256k1_address(1).parse().unwrap(), None) + .unwrap(), Role::Leader ); assert_eq!( manifest - .validate_node(7, &follower, Some(Role::Follower)) + .validate_node( + 7, + &follower, + secp256k1_address(2).parse().unwrap(), + Some(Role::Follower), + ) .unwrap(), Role::Follower ); @@ -438,17 +506,27 @@ mod tests { .unwrap(); let follower = PrivateKey::from_seed(2).public_key(); assert!(matches!( - valid.validate_node(7, &follower, Some(Role::Leader)), + valid.validate_node( + 7, + &follower, + secp256k1_address(2).parse().unwrap(), + Some(Role::Leader), + ), Err(ManifestError::RoleMismatch { .. }) )); assert!(matches!( - valid.validate_node(8, &follower, None), + valid.validate_node(8, &follower, secp256k1_address(2).parse().unwrap(), None,), Err(ManifestError::ZoneIdMismatch { .. }) )); let unknown = PrivateKey::from_seed(99).public_key(); assert!(matches!( - valid.validate_node(7, &unknown, None), + valid.validate_node(7, &unknown, secp256k1_address(99).parse().unwrap(), None,), Err(ManifestError::LocalNodeNotFound(_)) )); + + assert!(matches!( + valid.validate_node(7, &follower, secp256k1_address(3).parse().unwrap(), None,), + Err(ManifestError::LocalSecp256k1AddressMismatch { .. }) + )); } } diff --git a/crates/p2p/src/runtime.rs b/crates/p2p/src/runtime.rs index 8abb150eb..79169983a 100644 --- a/crates/p2p/src/runtime.rs +++ b/crates/p2p/src/runtime.rs @@ -1,5 +1,6 @@ use std::{net::SocketAddr, path::Path, sync::Arc, time::Duration}; +use alloy_primitives::Address as EthereumAddress; use commonware_cryptography::ed25519::PublicKey; use commonware_p2p::{ AddressableManager as _, Receiver as _, Recipients, Sender as _, authenticated::lookup, @@ -11,7 +12,7 @@ use tracing::{debug, error, info, warn}; use crate::{ P2pNetworkId, Role, ZoneManifest, - identity::Ed25519Identity, + identity::{Ed25519Identity, Secp256k1Identity}, network::{self, BLOCK_BACKLOG, BLOCK_CHANNEL, MAX_MESSAGE_SIZE}, }; @@ -26,6 +27,8 @@ const EVENT_BACKLOG: usize = 128; pub struct P2pConfig { manifest: Arc, ed25519_identity: Ed25519Identity, + // This individual node key will be used to sign zone blocks for the on-chain quorum. + secp256k1_identity: Secp256k1Identity, listen: SocketAddr, bypass_ip_check: bool, role: Role, @@ -37,22 +40,26 @@ impl P2pConfig { pub fn load( manifest_path: impl AsRef, ed25519_key_path: impl AsRef, + secp256k1_key_path: impl AsRef, listen: SocketAddr, bypass_ip_check: bool, expected_zone_id: u32, asserted_role: Option, ) -> eyre::Result { let ed25519_identity = Ed25519Identity::read_from_file(ed25519_key_path)?; + let secp256k1_identity = Secp256k1Identity::read_from_file(secp256k1_key_path)?; let manifest = ZoneManifest::read_from_file(manifest_path)?; validate_ip_check_configuration(&manifest, bypass_ip_check)?; let role = manifest.validate_node( expected_zone_id, &ed25519_identity.ed25519_public_key(), + secp256k1_identity.address(), asserted_role, )?; Ok(Self { manifest: Arc::new(manifest), ed25519_identity, + secp256k1_identity, listen, bypass_ip_check, role, @@ -69,6 +76,11 @@ impl P2pConfig { self.ed25519_identity.ed25519_public_key() } + /// This node's address derived from its individual secp256k1 key. + pub fn secp256k1_address(&self) -> EthereumAddress { + self.secp256k1_identity.address() + } + /// Local socket bound by Commonware. pub const fn listen(&self) -> SocketAddr { self.listen @@ -80,6 +92,7 @@ impl std::fmt::Debug for P2pConfig { f.debug_struct("P2pConfig") .field("zone_id", &self.manifest.zone_id()) .field("ed25519_public_key", &self.ed25519_public_key()) + .field("secp256k1_address", &self.secp256k1_address()) .field("listen", &self.listen) .field("bypass_ip_check", &self.bypass_ip_check) .field("role", &self.role) @@ -407,7 +420,11 @@ mod tests { use commonware_cryptography::{Signer as _, ed25519::PrivateKey}; use super::{P2pCommand, P2pConfig, P2pEvent, spawn_p2p, validate_ip_check_configuration}; - use crate::{P2pNetworkId, ZoneManifest, identity::Ed25519Identity, network::MAX_MESSAGE_SIZE}; + use crate::{ + P2pNetworkId, ZoneManifest, + identity::{Ed25519Identity, Secp256k1Identity}, + network::MAX_MESSAGE_SIZE, + }; fn available_address() -> SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); @@ -419,6 +436,10 @@ mod tests { Ed25519Identity::from_hex(&const_hex::encode_prefixed(key.encode().as_ref())).unwrap() } + fn secp256k1_identity(seed: u64) -> Secp256k1Identity { + Secp256k1Identity::from_hex(&format!("0x{seed:064x}")).unwrap() + } + #[test] fn dns_manifest_requires_explicit_ip_check_bypass() { let identities = [ @@ -431,9 +452,11 @@ mod tests { const_hex::encode_prefixed(identities[0].ed25519_public_key().as_ref()) ); for (index, identity) in identities.iter().enumerate() { + let secp256k1_identity = secp256k1_identity(index as u64 + 1); input.push_str(&format!( - "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\naddress = \"node-{index}.zone.local:9200\"\n", - const_hex::encode_prefixed(identity.ed25519_public_key().as_ref()) + "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\nsecp256k1_address = \"{}\"\naddress = \"node-{index}.zone.local:9200\"\n", + const_hex::encode_prefixed(identity.ed25519_public_key().as_ref()), + secp256k1_identity.address(), )); } let manifest = ZoneManifest::parse(&input).unwrap(); @@ -460,23 +483,33 @@ mod tests { const_hex::encode_prefixed(identities[0].ed25519_public_key().as_ref()) ); for (index, (identity, address)) in identities.iter().zip(addresses).enumerate() { + let secp256k1_identity = secp256k1_identity(index as u64 + 1); input.push_str(&format!( - "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\naddress = \"{address}\"\n", - const_hex::encode_prefixed(identity.ed25519_public_key().as_ref()) + "\n[[nodes]]\nname = \"node-{index}\"\ned25519_public_key = \"{}\"\nsecp256k1_address = \"{}\"\naddress = \"{address}\"\n", + const_hex::encode_prefixed(identity.ed25519_public_key().as_ref()), + secp256k1_identity.address(), )); } let manifest = Arc::new(ZoneManifest::parse(&input).unwrap()); let mut handles = identities .into_iter() .zip(addresses) - .map(|(identity, listen)| { + .enumerate() + .map(|(index, (identity, listen))| { + let secp256k1_identity = secp256k1_identity(index as u64 + 1); let role = manifest - .validate_node(9, &identity.ed25519_public_key(), None) + .validate_node( + 9, + &identity.ed25519_public_key(), + secp256k1_identity.address(), + None, + ) .unwrap(); spawn_p2p( P2pConfig { manifest: manifest.clone(), ed25519_identity: identity, + secp256k1_identity, listen, bypass_ip_check: false, role,