Repository navigation
feat(cli): store a generated node key in the data directory - #726
MegaRedHand wants to merge 1 commit into
Conversation
Without --node-key the node used a fresh in-memory key on every start, so its PeerId and ENR changed on each restart. On `beacon` that also moved its custody columns: the sidecars already on disk belonged to a node id nobody would select again, which is why startup warned about it. Now an absent --node-key means `<data-dir>/node.key`: read when present, generated and stored (hex, mode 0600, through a renamed `.tmp` sibling) on the directory's first start. The identity lives and dies with the data it selects. A stored file that fails to load is an error, never replaced, since that would silently discard a working identity. Both --node-key and the stored file are now checked to hold a valid secp256k1 key at load, so a bad one is reported with its path instead of panicking in build_swarm. Key resolution moves below the RocksDB open, so the DB's lock keeps two processes on one directory from racing to write the key. The custody warning in `beacon::wire_params` is removed with its `node_key_supplied` flag, since the identity is now always stable.
🤖 Kimi Code ReviewI'll review this PR which changes node key handling from ephemeral in-memory generation to persistent storage in the data directory. Let me analyze the changes across all files. Security ReviewCritical: Race Condition in Key Generation (bin/ethlambda/src/main.rs:1350-1355)let generated = secp256k1::SecretKey::new(&mut secp256k1::rand::rngs::OsRng).secret_bytes();
let contents = format!("{}\n", hex::encode(generated));
let temporary = data_dir.join(format!("{STORED_NODE_KEY_FILE}.tmp"));
ethlambda_validator::secure_fs::write_private(&temporary, contents)
.and_then(|()| std::fs::rename(&temporary, &stored))The comment states "RocksDB's lock is what stops two processes started on it from both generating a key and racing to store theirs." However, RocksDB's file lock does NOT prevent concurrent access to other files in the same directory. Two processes can still race:
The Fix: Use an atomic create-if-not-exists operation, or better, use High: Missing Error Context Chain (bin/ethlambda/src/main.rs:1345-1348)let exists = stored
.try_exists()
.wrap_err_with(|| format!("failed to look for a node key at {}", stored.display()))?;
Correctness IssuesMedium: Hex Format Inconsistency (bin/ethlambda/src/main.rs:1350)let contents = format!("{}\n", hex::encode(generated));The stored format adds a newline, but Low:
|
| Severity | Issue | Location |
|---|---|---|
| Critical | Race condition in key generation despite RocksDB lock claim | main.rs:1340-1355 |
| High | try_exists() conflates "doesn't exist" with "permission denied" |
main.rs:1345-1348 |
| Medium | Implicit newline handling in hex format | main.rs:1350 |
| Low | read_node_key returns raw bytes after validation |
main.rs:1367-1372 |
The core design is sound and well-documented, but the race condition needs addressing before merge. Consider using std::fs::OpenOptions::new().create_new(true) or a proper file lock for the key file itself.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Claude Code ReviewReview of PR 726: store a generated node key in the data directory I read The design is sound. Key resolution runs after the RocksDB open, so the DB lock serializes first-start generation. A stored file that fails to load is an error rather than being replaced. The key is validated at load, so a bad key no longer panics in Findings
Overall this is good to merge once the stale comment (Item 1) is fixed. The fsync and Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code Review
Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🗒️ Description / Motivation
Without
--node-key, both sub-commands generated a fresh secp256k1 key in memory on every start, so the node's PeerId and ENR changed on each restart. A lean node lost its place in every peer's scoring. Onbeaconit was worse: the custody columns are a function of the node id, so after a restart the sidecars already on disk belonged to a custody set nobody would select again, and the new id had nothing custodied for it yet. Startup warned about both, and the README told operators to generate a key withopensslto avoid it.This PR makes the node keep its own identity: when
--node-keyis absent, it stores a generated key in--data-dirand reads it back on every later start.What Changed
bin/ethlambda/src/main.rsresolve_node_key(cli, data_dir):--node-keyif given, else<data-dir>/node.key, generating and storing it on the directory's first start. Resolution moves below the RocksDB open. Newread_node_keyvalidates the bytes as a secp256k1 keybin/ethlambda/src/beacon.rswire_paramsdrops itsnode_key_suppliedparameter and the custody warning it gatedcrates/validator/src/secure_fs.rssecure_fsmodule andwrite_privatego frompub(crate)topub, so the binary writes the key with the same0600helper the validator client uses for its secretsbin/ethlambda/src/cli.rs,docs/{cli,architecture,data_storage}.md,CLAUDE.mdREADME.mdopensslor pass--node-keyCorrectness / Behavior Guarantees
--node-keywins. Passing it on a directory that already holdsnode.keyuses the supplied key and leaves the stored one untouched.--node-keyused to reachbuild_swarmand panic withinvalid node key; now it is a startup error naming the file.--node-key. The stored file is hex with a trailing newline, so it can be passed as--node-keyto another run.RocksDBBackend::open, so two processes started on one directory cannot both generate a key and race to store it. The.tmp+ rename means an interrupted first start leaves no partial key.rocksdb_reopens_a_data_dir_holding_a_stored_node_keypins that. Wiping the directory resets the identity together with the custody columns it selected.beacon::wire_paramsis removed because the identity is now always stable across restarts.Tests Added / Run
Added in
bin/ethlambda/src/main.rs:a_supplied_node_key_is_read_verbatim_and_nothing_is_storeda_missing_node_key_is_generated_once_and_reloaded_from_the_data_dir(also checks that the stored file reads back as--node-key)a_generated_node_key_is_stored_at_mode_0600(unix)a_stored_node_key_that_fails_to_load_is_an_error_not_a_new_keyrocksdb_reopens_a_data_dir_holding_a_stored_node_keyRun:
cargo test -p ethlambda --profile release-fast --binscargo test -p ethlambda-validator --profile release-fast --libRelated Issues / PRs
None.
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — not run in full; only the two touched crates' unit tests above