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
12 changes: 8 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,14 @@ where a bad value is reported.
`run_node` owns everything that is not chain-specific, in order: the discv5
port check, metrics registration, the banner and version log, the
`RLIMIT_NOFILE` raise, the `HIVE_LEAN_TEST_DRIVER` early return (lean-only, and
it must still precede key loading), `--node-key` resolution, resolving
`--network` into a `NetworkSource` (mainnet only; must precede the next step,
since a loaded network's own bootnode list is part of its fallback), reading
`--bootnodes`, and building the aggregator, sync-status and event handles.
it must still precede key loading), resolving `--network` into a
`NetworkSource` (mainnet only; must precede the next step, since a loaded
network's own bootnode list is part of its fallback), reading `--bootnodes`,
building the aggregator, sync-status and event handles, opening `--data-dir`'s
RocksDB backend, and `--node-key` resolution. That last one comes after the
open because, without `--node-key`, the identity is `<data-dir>/node.key`,
generated and stored on the directory's first start, and RocksDB's lock is
what keeps two processes on one directory from racing to write it.

It then branches **once**, on `network`, both arms inline, each evaluating to a
`ChainSetup`: the wire configuration, the ENR entries that describe it, the
Expand Down
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ mv ethlambda-linux-x86_64 ethlambda && mv ethrex-linux-x86_64 ethrex
./ethlambda --version && ./ethrex --version

mkdir -p beacon-data ethrex-data
# A persisted key keeps the node's identity, and so its custody set, stable across restarts
openssl rand -hex 32 > beacon-data/node-key
# The secret both clients authenticate the Engine API with
openssl rand -hex 32 > jwt.hex

Expand All @@ -51,7 +49,6 @@ openssl rand -hex 32 > jwt.hex
./ethlambda beacon \
--network mainnet \
--checkpoint-sync-url https://beaconstate.ethstaker.cc \
--node-key beacon-data/node-key \
--data-dir beacon-data/db \
--discovery.advertise-ip "$(curl -s https://ifconfig.me)" \
--execution-endpoint http://127.0.0.1:8551 \
Expand Down Expand Up @@ -129,8 +126,6 @@ docker pull ghcr.io/lambdaclass/ethlambda:latest
docker pull ghcr.io/lambdaclass/ethrex

mkdir -p beacon-data ethrex-data
# A persisted key keeps the node's identity, and so its custody set, stable across restarts
openssl rand -hex 32 > beacon-data/node-key
# The secret both clients authenticate the Engine API with
openssl rand -hex 32 > jwt.hex
# This host's public address, published in the node's ENR (see above)
Expand Down Expand Up @@ -159,7 +154,6 @@ docker run -d --name ethlambda-beacon --network ethereum --stop-signal SIGINT \
ghcr.io/lambdaclass/ethlambda:latest beacon \
--network mainnet \
--checkpoint-sync-url https://beaconstate.ethstaker.cc \
--node-key /data/node-key \
--data-dir /data/db \
--http-address 0.0.0.0 \
--discovery.advertise-ip "$PUBLIC_IP" \
Expand All @@ -177,8 +171,6 @@ and no `--execution-*` flags.
docker pull ghcr.io/lambdaclass/ethlambda:latest

mkdir -p beacon-data
# A persisted key keeps the node's identity, and so its custody set, stable across restarts
openssl rand -hex 32 > beacon-data/node-key
# This host's public address, published in the node's ENR (see above)
PUBLIC_IP=$(curl -s https://ifconfig.me)

Expand All @@ -190,7 +182,6 @@ docker run -d --name ethlambda-beacon --stop-signal SIGINT \
ghcr.io/lambdaclass/ethlambda:latest beacon \
--network mainnet \
--checkpoint-sync-url https://beaconstate.ethstaker.cc \
--node-key /data/node-key \
--data-dir /data/db \
--http-address 0.0.0.0 \
--discovery.advertise-ip "$PUBLIC_IP"
Expand Down Expand Up @@ -293,15 +284,14 @@ what they sign.
./target/release/ethlambda beacon \
--network mainnet \
--checkpoint-sync-url <beacon-api-url> \
--node-key ./node-key \
--data-dir ./data
```

| Flag | Meaning |
|---|---|
| `--network` | `mainnet` (default), `sepolia`, `hoodi`, `plataberget`, or a path to a directory of network files (`config.yaml`, `genesis.ssz`, optionally `bootstrap_nodes.yaml`), the layout `eth-clients` publishes and kurtosis mounts |
| `--checkpoint-sync-url` | Where the anchor comes from. Required on a fresh data directory for a built-in network, since those never start from genesis. A resumable data directory is used before it; a network loaded from a directory anchors at its own `genesis.ssz` when no URL is given |
| `--node-key` | Optional, but worth persisting: the columns this node custodies are a function of its node id, so without a key file it changes identity, and custody set, on every restart |
| `--node-key` | Optional. Without it, the node generates a key on the data directory's first start and keeps it there as `node.key`, so its identity, and with it the columns it custodies, stays the same across restarts |
| `--execution-endpoint`, `--execution-jwt-secret` | Optional Engine API pairing, given together. Without them, blocks import without payload validation |

A built-in network needs nothing else on disk. Its `eth-clients` `config.yaml`
Expand Down
53 changes: 14 additions & 39 deletions bin/ethlambda/src/beacon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub struct BeaconWireParams {
pub fork_id: EnrForkId,
}

/// The discv5 node id for this process's `--node-key` bytes.
/// The discv5 node id for this process's node key bytes.
///
/// A thin pass-through, kept as its own function so `run_node`'s exact
/// composition β€” resolve the key, derive the id, feed it to [`wire_params`] β€”
Expand All @@ -141,15 +141,9 @@ pub fn beacon_node_id(node_key: &[u8]) -> eyre::Result<[u8; 32]> {
/// custody set off the identity we publish. The anchor state itself belongs to
/// the anchor-and-follow work, and must be checked against the genesis values
/// used here when it lands.
///
/// `node_key_supplied` carries no key material, only whether `node_id` came
/// from a persisted `--node-key` or one generated fresh for this run: this
/// function has no other way to tell the two apart, and that is exactly what
/// the startup warning below needs to know.
pub fn wire_params(
source: &crate::network::NetworkSource,
node_id: [u8; 32],
node_key_supplied: bool,
custody_group_count: u64,
) -> eyre::Result<BeaconWireParams> {
let chain = source.config().clone();
Expand Down Expand Up @@ -206,9 +200,9 @@ pub fn wire_params(
}

// The node id is the discv5 one, so what this node custodies here is what
// any peer computes for it from its ENR. A node without a persistent
// --node-key gets a new identity and therefore a new custody set on every
// restart, which is why startup warns about it right below.
// any peer computes for it from its ENR. It stays the same across restarts
// because the node key does: `--node-key`, or the key `resolve_node_key`
// stored in the data directory beside the columns it custodies.
//
// `sampling_size` is the larger of the advertised count and
// `SAMPLES_PER_SLOT`, so the default advertisement still custodies
Expand All @@ -225,28 +219,9 @@ pub fn wire_params(
"Custodying data columns"
);

// The columns above are stored under this identity and served to peers
// who compute the same set from it. An ephemeral identity makes both
// sides of that agreement stale on the next restart: the sidecars already
// on disk belong to a node id nobody, including this node, will select
// again, and the fresh id this run advertises has nothing custodied for
// it yet. Placed next to the custody log line above so an operator reads
// the two together rather than finding this warning buried in startup
// noise.
if !node_key_supplied {
warn!(
"No --node-key supplied: this node's custody columns are a function of its \
discv5 node id, so a fresh identity on every restart means the columns already \
stored on disk belong to a different custody set than the one this run \
advertises and serves. Pass --node-key with a persisted key file to keep one \
identity, and therefore one custody set, across restarts."
);
}

// The same node id again, and the same consequence of an ephemeral one:
// `p2p-interface.md` makes this a public function of the node id so that a
// peer can compute what this node should be listening to from its ENR
// alone. Computed at the current epoch and then kept for the process's
// The same node id again: `p2p-interface.md` makes this a public function
// of the node id so that a peer can compute what this node should be
// listening to from its ENR alone. Computed at the current epoch and then kept for the process's
// lifetime; see `subnets`'s module documentation for why this does not
// rotate.
let attestation_subnets =
Expand Down Expand Up @@ -352,8 +327,8 @@ mod tests {
#[test]
fn the_wire_parameters_are_derived_offline() {
let source = crate::network::NetworkSource::built_in_mainnet().unwrap();
let params = wire_params(&source, [0x11; 32], true, CUSTODY_REQUIREMENT)
.expect("no network is needed");
let params =
wire_params(&source, [0x11; 32], CUSTODY_REQUIREMENT).expect("no network is needed");
assert_eq!(params.wire.genesis_time, MAINNET_GENESIS_TIME);
// The digest is whatever fork the wall clock lands in, so it is not
// pinned here; that it agrees with the ENR entry is the invariant.
Expand All @@ -374,12 +349,12 @@ mod tests {

let source = crate::network::NetworkSource::built_in_mainnet().unwrap();

let a = wire_params(&source, [0x11; 32], true, CUSTODY_REQUIREMENT)
.expect("no network is needed");
let a =
wire_params(&source, [0x11; 32], CUSTODY_REQUIREMENT).expect("no network is needed");
assert_eq!(a.wire.custody_columns.len(), sampling as usize);

let b = wire_params(&source, [0x22; 32], true, CUSTODY_REQUIREMENT)
.expect("no network is needed");
let b =
wire_params(&source, [0x22; 32], CUSTODY_REQUIREMENT).expect("no network is needed");
assert_ne!(a.wire.custody_columns, b.wire.custody_columns);
}

Expand All @@ -391,7 +366,7 @@ mod tests {
let node_id = beacon_node_id(&[0x33; 32]).expect("a well-formed key");
let source = crate::network::NetworkSource::built_in_mainnet().unwrap();
let params =
wire_params(&source, node_id, true, CUSTODY_REQUIREMENT).expect("no network is needed");
wire_params(&source, node_id, CUSTODY_REQUIREMENT).expect("no network is needed");
let sampling = ethlambda_state_transition::beacon::das::sampling_size(
ethlambda_types::beacon::constants::CUSTODY_REQUIREMENT,
);
Expand Down
15 changes: 8 additions & 7 deletions bin/ethlambda/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ pub(crate) struct CommonOptions {
/// Hex file holding the secp256k1 key that is this node's libp2p and
/// discv5 identity.
///
/// Optional on both sub-commands. When omitted, a fresh key is generated
/// in memory at startup (logged as a warning) and used for that run only,
/// so the PeerId and ENR differ on the next start. Pass a persisted key
/// file for a stable identity.
/// Optional on both sub-commands. When omitted, the key stored in
/// `--data-dir` as `node.key` is used, and on the directory's first start
/// one is generated and stored there (hex, mode `0600`), so the PeerId
/// and ENR stay the same across restarts until the directory is wiped.
#[arg(long)]
pub(crate) node_key: Option<PathBuf>,
/// Path to a bootnode list: ENRs, one per YAML entry.
Expand Down Expand Up @@ -894,9 +894,10 @@ mod tests {

#[test]
fn an_absent_node_key_parses_on_both_sub_commands() {
// Both generate an ephemeral in-memory key instead (`main`'s
// `resolve_node_key`), so neither rejects the argv. On `node` that is
// a behavior change: `--node-key` used to be required there.
// Both use a key stored in `--data-dir` instead, generating it on
// the first start (`main`'s `resolve_node_key`), so neither rejects
// the argv. On `node` that is a behavior change: `--node-key` used to
// be required there.
assert_eq!(
parse_node(without(&base_args(), "--node-key"))
.common
Expand Down
Loading
Loading