Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 32 additions & 2 deletions crates/node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ fn run_node(mut cli: Cli<ZoneChainSpecParser, ZoneArgs>) -> 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,
Expand All @@ -123,6 +127,7 @@ fn run_node(mut cli: Cli<ZoneChainSpecParser, ZoneArgs>) -> 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"
);
Expand Down Expand Up @@ -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<PathBuf>,
Expand All @@ -232,6 +237,15 @@ pub struct ZoneArgs {
)]
pub p2p_key: Option<PathBuf>,

/// 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<PathBuf>,

/// Socket address bound for multi-sequencer Commonware traffic.
#[arg(
long = "p2p.listen",
Expand Down Expand Up @@ -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",
Expand All @@ -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();
Expand Down Expand Up @@ -472,6 +500,8 @@ mod tests {
"zone.toml",
"--p2p.key",
"node.key",
"--secp256k1.key",
"node-secp256k1.key",
"--p2p.bypass-ip-check",
]))
.unwrap();
Expand Down
40 changes: 33 additions & 7 deletions crates/node/tests/it/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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::<PrivateKeySigner>().unwrap());

let unique = NEXT_CHAIN_ID.fetch_add(1, Ordering::Relaxed);
let config_dir = std::env::temp_dir().join(format!(
Expand All @@ -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)?;
Expand All @@ -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,
Expand Down Expand Up @@ -2737,6 +2750,10 @@ pub(crate) fn leader_p2p_config(listen: SocketAddr) -> eyre::Result<P2pConfig> {
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::<PrivateKeySigner>().unwrap());
let addresses = [listen, available_address()?, available_address()?];
let config_dir = std::env::temp_dir().join(format!(
"tempo-zone-p2p-config-{}-{}",
Expand All @@ -2750,20 +2767,29 @@ pub(crate) fn leader_p2p_config(listen: SocketAddr) -> eyre::Result<P2pConfig> {
"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)?;
std::fs::write(
&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,
Expand Down
1 change: 1 addition & 0 deletions crates/p2p/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ workspace = true

[dependencies]
alloy-primitives.workspace = true
alloy-signer-local.workspace = true

commonware-codec.workspace = true
commonware-cryptography.workspace = true
Expand Down
15 changes: 8 additions & 7 deletions crates/p2p/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
68 changes: 60 additions & 8 deletions crates/p2p/src/identity.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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 _,
ed25519::{PrivateKey, PublicKey},
};

/// An Ed25519 private key loaded as a node's Commonware identity.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub(crate) struct Ed25519Identity(PrivateKey);

impl Ed25519Identity {
Expand Down Expand Up @@ -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<Path>) -> Result<Self, Secp256k1IdentityError> {
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<Self, Secp256k1IdentityError> {
encoded
.parse::<PrivateKeySigner>()
.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()
}
}

Expand All @@ -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() {
Expand All @@ -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"));
}
}
Loading
Loading