Skip to content

Devnet4 Phase 1: dual-key Validator, SignedBlock, genesis config format#230

Draft
pablodeymo wants to merge 1 commit intodevnet4from
devnet4-phase1-types
Draft

Devnet4 Phase 1: dual-key Validator, SignedBlock, genesis config format#230
pablodeymo wants to merge 1 commit intodevnet4from
devnet4-phase1-types

Conversation

@pablodeymo
Copy link
Collaborator

Motivation

First of 4 PRs implementing devnet4 (leanSpec#449). This PR introduces the SSZ-breaking type changes that all subsequent phases depend on.

Description

Validator struct: single pubkey → dual pubkeys

// BEFORE
pub struct Validator {
    pub pubkey: ValidatorPubkeyBytes,
    pub index: u64,
}

// AFTER
pub struct Validator {
    pub attestation_pubkey: ValidatorPubkeyBytes,
    pub proposal_pubkey: ValidatorPubkeyBytes,
    pub index: u64,
}

Each validator now has two independent XMSS keys — one for attestation signing and one for block proposal signing. This eliminates OTS conflicts when a proposer needs to sign both a block and an attestation in the same slot.

  • get_pubkey()get_attestation_pubkey() + get_proposal_pubkey()
  • SSZ field order matches spec: attestation_pubkey, proposal_pubkey, index

Block types: remove proposer attestation wrapper

Type Action
`SignedBlockWithAttestation` Renamed to `SignedBlock`, message is `Block` directly
`BlockWithAttestation` Deleted — proposer attestation no longer in block
`BlockSignaturesWithAttestation` Deleted — storage uses `BlockSignatures` directly

Genesis config format

# OLD
GENESIS_VALIDATORS:
  - "cd323f232b..."

# NEW
GENESIS_VALIDATORS:
  - attestation_pubkey: "cd323f232b..."
    proposal_pubkey: "0767e6592..."

Test fixtures

  • Test fixture Validator deserialization updated for dual pubkeys (attestationPubkey, proposalPubkey)

PR chain

  1. → Phase 1: Types (this PR)
  2. Phase 2: Key manager + block proposal (devnet4-phase2-proposal)
  3. Phase 3: Store + verification (devnet4-phase3-store)
  4. Phase 4: Network + tests (devnet4-phase4-network)

How to Test

This PR alone does not compile (SSZ-breaking). Full compilation requires all 4 phases merged.

Introduce the devnet4 type-level changes:

- Validator: single pubkey → attestation_pubkey + proposal_pubkey with
  get_attestation_pubkey() and get_proposal_pubkey() methods
- SignedBlockWithAttestation → SignedBlock (message is Block directly)
- Delete BlockWithAttestation and BlockSignaturesWithAttestation wrappers
- Genesis config: GENESIS_VALIDATORS changes from list of hex strings to
  list of {attestation_pubkey, proposal_pubkey} objects
- Test fixtures: Validator deserialization updated for dual pubkeys

NOTE: This is SSZ-breaking. Downstream crates will not compile until
subsequent phases update all call sites.
@github-actions
Copy link

🤖 Kimi Code Review

Review Summary

This PR introduces dual-key validator structure (attestation + proposal keys) and refactors block signing accordingly. The changes are well-structured and address the OTS constraint issue.

Issues Found

  1. Missing import in genesis.rs (line 130-135):

    • The deser_pubkey_hex function is used but not imported in the genesis module. Add:
    use crate::state::deser_pubkey_hex;
  2. Test data inconsistency (lines 59-61):

    • ATT_PUBKEY_C and PROP_PUBKEY_A have identical values, which seems like a copy-paste error. These should be distinct keys.
  3. Incomplete validation in Validator (state.rs lines 89-96):

    • Both get_attestation_pubkey() and get_proposal_pubkey() can return DecodeError, but there's no validation at construction time. Consider adding validation in GenesisConfig::validators() to fail fast on invalid keys.
  4. Potential SSZ compatibility issue:

    • The Validator struct's SSZ encoding changed (added proposal_pubkey field). Ensure all downstream consumers (database, networking) handle this gracefully during upgrades.

Positive Aspects

  • Clean separation of attestation and proposal keys addresses the OTS constraint properly
  • Good use of serde attributes for JSON compatibility (#[serde(rename)])
  • Maintained backward compatibility in test fixtures with proper field mapping
  • Removed unnecessary BlockWithAttestation complexity, simplifying the block structure

Minor Suggestions

  1. Consider adding a validator constructor that validates both keys:
impl Validator {
    pub fn new(attestation_pubkey: ValidatorPubkeyBytes, proposal_pubkey: ValidatorPubkeyBytes, index: u64) -> Result<Self, DecodeError> {
        // Validate both keys
        let _ = ValidatorPublicKey::from_bytes(&attestation_pubkey)?;
        let _ = ValidatorPublicKey::from_bytes(&proposal_pubkey)?;
        Ok(Self { attestation_pubkey, proposal_pubkey, index })
    }
}
  1. The commented-out state root assertions in genesis tests should either be restored with correct values or removed entirely to avoid confusion.

Overall, the PR correctly implements the dual-key validator design and simplifies the block structure appropriately.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

🤖 Claude Code Review

Review: Devnet4 Phase 1 — Dual-key Validator, SignedBlock, Genesis Config

This is a clean structural PR. The motivation (OTS key separation) is sound, and the deletions (removing the BlockWithAttestation/BlockSignaturesWithAttestation wrappers) simplify the design correctly. A few items worth addressing:


Correctness

Incomplete test coverage in parse_genesis_config (genesis.rs:88-115)

The test verifies validators[0].proposal_pubkey, but skips the proposal pubkeys for validators 1 and 2. Given that the whole point of this PR is dual-key correctness, those assertions should be there:

// Missing:
assert_eq!(config.genesis_validators[1].proposal_pubkey, hex::decode(PROP_PUBKEY_B)...);
assert_eq!(config.genesis_validators[2].proposal_pubkey, hex::decode(PROP_PUBKEY_C)...);

Duplicate constant values in test data (genesis.rs:56-59)

ATT_PUBKEY_C and PROP_PUBKEY_A hold the same hex string (b7b0f72e24...). Using identical byte values for conceptually distinct keys (one attestation, one proposal) in test fixtures is fragile — it can mask key-confusion bugs in downstream phases (e.g., accidentally using the wrong key for verification). All six key slots in the test config should use distinct values.

Weakened state root pin (genesis.rs:139-157)

Replacing the exact hash assertion with a non-zero check is understandable for a non-compiling PR. However, the comment says "Will be recomputed once we can run this test" without a tracking mechanism. A // TODO(devnet4-phase4): restore exact root pin comment pointing to the follow-up PR would make this less likely to slip through.


Minor / Non-blocking

Error message quality regression in deser_pubkey_hex (genesis.rs:47-55)

The old deser_hex_pubkeys produced "GENESIS_VALIDATORS[{idx}] is not valid hex" with the validator index. The new per-field deserializer drops that context, emitting only "pubkey is not valid hex". Serde does add field path context, but an explicit "attestation_pubkey" / "proposal_pubkey" hint in the error message would help operators debugging a malformed config file. Low priority.


What looks good

  • SSZ field order (attestation_pubkey, proposal_pubkey, index) matches spec, and the breaking nature is correctly acknowledged.
  • Deletions of BlockWithAttestation and BlockSignaturesWithAttestation are complete — no dead references left in this layer.
  • The deser_pubkey_hex refactor (from a vec-level to per-field deserializer) is cleaner and the ownership model is right.
  • The get_pubkey() TODO comment was quietly cleaned up — no objections there.
  • Debug impl for SignedBlock carries over correctly from SignedBlockWithAttestation.
  • Test fixture serde renaming (attestationPubkey / proposalPubkey) is correct camelCase for the JSON fixture format.

Automated review by Claude (Anthropic) · custom prompt

@github-actions
Copy link

🤖 Codex Code Review

  1. Build-breaking public API change not propagated (high).
    SignedBlockWithAttestation/BlockWithAttestation were removed/renamed in block.rs:15, but downstream crates still import/use old types, e.g. blockchain/lib.rs:10, storage/store.rs:17, net/api/src/lib.rs:3.
    This PR diff is internally inconsistent unless companion changes exist elsewhere.

  2. Validator key API split not propagated to attestation/proposal verification paths (high).
    Validator::get_pubkey was replaced by get_attestation_pubkey/get_proposal_pubkey in state.rs:89, but call sites still use get_pubkey, e.g. blockchain/store.rs:173.
    Beyond compile failure, this is consensus-critical: attestation verification must use the attestation key and block proposal verification must use the proposal key.

  3. Consensus-regression test was weakened too much (medium).
    state_from_genesis_root no longer pins expected SSZ roots and now only checks “non-zero” in genesis.rs:142 and genesis.rs:151.
    For consensus code, this loses regression detection for TreeHash/SSZ layout and should be replaced with updated fixed expected roots.

  4. Missing safety validation for dual XMSS keys (medium).
    Genesis mapping accepts any pair of keys without validation in genesis.rs:23.
    Given the stated OTS rationale, add a check that attestation_pubkey != proposal_pubkey (or explicitly document why equal keys are allowed), otherwise operators can silently configure an unsafe setup.

  5. Test coverage gap for proposal key parsing (low).
    Deserialization test checks only validator 0 proposal key in genesis.rs:94, not validators 1/2 proposal keys. Add assertions for all entries to catch field-mapping regressions.

I could not run cargo check in this environment due rustup temp-file permission restrictions, so findings are from diff + repository cross-reference.


Automated review by OpenAI Codex · custom prompt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant