Devnet4: separate attestation and proposal keys#229
Conversation
…al keys) This commit introduces the dual-key model where validators have distinct attestation and proposal XMSS keys. Test fixture generation is blocked until leansig-test-keys publishes keys in the new format.
🤖 Kimi Code ReviewReview SummaryThis PR introduces dual XMSS key pairs for validators (attestation + proposal keys) and removes the proposer attestation wrapper from blocks. The changes are substantial and touch consensus-critical code. Critical Issues
Security Concerns
Performance Impact
Code Quality Issues
Recommendations
The changes appear architecturally sound for the dual-key requirement, but need the above issues addressed before production use. Automated review by Kimi (Moonshot AI) · custom prompt |
🤖 Codex Code Review
Overall, the dual-key separation (attestation vs proposal key) and proposal-signature verification over block root look directionally correct; main concerns are upgrade safety and test/guardrail regressions. Automated review by OpenAI Codex · custom prompt |
🤖 Claude Code ReviewNow I have a complete picture of the PR. Let me write the review. PR Review: Devnet4 Dual-Key ModelOverall assessment: The implementation correctly tracks the spec change and the design is clean — removing Correctness IssuesWrong metric used for proposal signing
let _timing = metrics::time_pq_sig_attestation_signing(); // <-- wrong
key_pair.proposal_key.sign(slot, message) ...Also, Stale SSZ fixture in gossipsub encoding test
let block_bytes = include_bytes!("../../test_data/signed_block_with_attestation.ssz");
let _block = SignedBlock::from_ssz_bytes(block_bytes).unwrap();The binary fixture was captured from Zeam with the old Weakened genesis root pinning test
assert_ne!(root, crate::primitives::H256::ZERO, "state root should be non-zero");The previous test pinned exact byte values for both Design / API IssuesUnused parameter should be removed, not prefixed
fn on_block_core(
store: &mut Store,
signed_block: SignedBlock,
verify: bool,
_local_validator_ids: &[u64], // <-- effectively dead
) -> Result<(), StoreError> {All three call sites (
pub struct ValidatorKeyPair {
pub attestation_key: ValidatorSecretKey,
pub proposal_key: ValidatorSecretKey,
}The whole motivation of the dual-key model is to prevent a single XMSS key from being used for two different purposes in the same slot. There is no check preventing
#[serde(rename = "attestation_pubkey_hex")]
#[serde(deserialize_with = "deser_pubkey_hex")]
_attestation_pubkey: ValidatorPubkeyBytes,
#[serde(rename = "proposal_pubkey_hex")]
#[serde(deserialize_with = "deser_pubkey_hex")]
_proposal_pubkey: ValidatorPubkeyBytes,These bytes are deserialized correctly but never compared against the public key derived from the loaded secret key. The intent appears to be "parse to validate format", but if the key file and the hex string belong to different validators (misconfiguration) this will go undetected. A comparison of the pubkey derived from the secret key against the annotated hex value would catch config mismatches at startup — the same way checkpoint sync already validates Minor Nits
What Looks Good
Automated review by Claude (Anthropic) · custom prompt |
|
Splitting into 4 smaller PRs targeting devnet4 branch. See linked PRs below. |
Motivation
leanSpec commit
ad9a322introduces the devnet4 spec: separate attestation and proposal keys. Each validator now has two independent XMSS keys — one for attestation signing and one for block proposal signing.This solves a fundamental OTS (one-time signature) conflict: in devnet3, a proposer needed to sign both its attestation and the block using the same XMSS key in the same slot. With dual keys, the attestation key and proposal key advance independently, eliminating this constraint.
Additionally, the proposer's attestation is removed from the block structure entirely. The proposer now attests through the normal gossip pipeline at interval 1, like every other validator. This simplifies block processing and removes a circular fork-choice weight advantage where the proposer's attestation could influence its own block's weight.
Description
Commit 1: Upgrade leanSpec to ad9a322
Updates
LEAN_SPEC_COMMIT_HASHin the Makefile. Fixture generation is blocked untilleansig-test-keyspublishes keys in the new dual-key format.Commit 2: Implement devnet4 dual-key model
23 files changed, 326 insertions, 500 deletions — net reduction of 174 lines because the proposer attestation wrapper was removed.
Changes organized by area:
Core Types (
crates/common/types/)Validatorstructpubkey→attestation_pubkey+proposal_pubkey. New methodsget_attestation_pubkey()andget_proposal_pubkey()SignedBlockWithAttestation→SignedBlockmessageis nowBlockdirectly (wasBlockWithAttestation)BlockWithAttestationBlockSignaturesWithAttestationBlockSignaturesdirectlyBlockSignatures.proposer_signatureGENESIS_VALIDATORSchanged from list of hex strings to list of{attestation_pubkey, proposal_pubkey}objectsKey Manager (
crates/blockchain/src/key_manager.rs)ValidatorKeyPairstruct holdingattestation_keyandproposal_keyKeyManagerstoresHashMap<u64, ValidatorKeyPair>instead of single keyssign_block_root()method that uses the proposal keysign_attestation()now internally routes to the attestation keyBlock Proposal (
crates/blockchain/src/lib.rs)propose_block(): signs block root with proposal key, no longer creates proposer attestationproduce_attestations(): removed proposer skip — all validators (including proposer) attest at interval 1SignedBlockWithAttestation→SignedBlocktype cascade.message.block.X→.message.Xfield access fixesStore & Verification (
crates/blockchain/src/store.rs)verify_signatures(): attestation proofs verified withget_attestation_pubkey(), proposer signature verified withget_proposal_pubkey()over block rooton_block_core(): removed entire proposer attestation processing block (~40 lines) — no more gossip signature insert, no more dummy proof for proposerStoreError::ProposerAttestationMismatchvariantget_attestation_pubkey()Storage (
crates/storage/src/store.rs)write_signed_block(): destructuresSignedBlock { message: block, signature }directly, storesBlockSignatureswithout wrapperget_signed_block(): deserializesBlockSignaturesdirectly, reconstructsSignedBlockNetwork (
crates/net/).message.block.X→.message.Xin gossip handler and req_resp handlersBinary (
bin/ethlambda/src/main.rs)AnnotatedValidator: now expectsattestation_pubkey_hex,proposal_pubkey_hex,attestation_privkey_file,proposal_privkey_fileread_validator_keys(): reads two key files per validator, constructsValidatorKeyPairattestation_pubkeyandproposal_pubkeymatchTests
BlockStepData: removedproposer_attestationfieldTestSignedBlock: simplified fromTestSignedBlockWithAttestationBlockers
leansig-test-keyshas not published dual-key format keys yet. Fixture generation (make leanSpec/fixtures) fails withKeyError: 'attestation_public'. Spec tests (fork_choice, verify_signatures, stf) cannot run until keys are available.Potential split
This PR could be split into smaller parts if preferred:
Validatordual pubkeys,SignedBlockrename, genesis config format (Phase 1)ValidatorKeyPair,sign_block_root, proposer attestation removal (Phase 2)on_block_core(Phase 3)Note: Phase 1 is SSZ-breaking, so Phases 2-4 won't compile independently without Phase 1.
How to Test
Full devnet testing requires lean-quickstart devnet4 support.