fix(devnet5): don't register block-attestation data at import (broke Type-2 merge)#395
Conversation
…laceholders
Block import previously inserted T1::empty(bits) placeholders (participant
bits, empty proof bytes) into the known payload pool. Block building read the
same pool and fed those empty bytes into the Type-2 merge, where lean-multisig
decompression failed ("child proof deserialization failed at index N").
Match the spec's on_block: register each block attestation's data key with an
empty proof set instead of a proof-less placeholder. No empty-byte proof ever
enters the pool, so the merge can no longer be handed one. Block-borne votes
now contribute zero fork-choice weight at import; the weight returns once
reaggregation recovers real Type-1 proofs into the pool (deferred by up to one
slot), matching the spec.
Known follow-up: ethlambda's import-only forkchoice harness expects block votes
in "known" immediately after import (the spec's harness simulates the proposer
build to populate them). 13 forkchoice spec tests now fail on this expectation;
the harness/reaggregation wiring will be addressed separately.
Option A made on_block spec-faithful: it registers each block attestation's data key with an empty proof set, so block-borne votes contribute no fork-choice weight at import. A real node recovers that weight by SNARK-splitting the block's merged Type-2 proof into per-attestation Type-1s (reaggregation); leanSpec's forkchoice harness gets the same effect by simulating the proposer build straight into the known pool. ethlambda's harness only imports blocks (on_block_without_verification) with blank proofs, so it did neither and 13 head/reorg/safe-target fixtures lost their vote source. Reconstruct structurally after import: for each block attestation, insert a Type-1 carrying its aggregation_bits into the known pool (fork choice reads participants, not proof bytes; the harness never feeds these to a Type-2 merge). Recompute the head afterward, since on_block ran its head update before these votes existed. Expose update_head for the harness recompute. All 84 forkchoice tests pass.
🤖 Kimi Code ReviewCritical Consensus Considerations
Security Observations
Code Quality and Correctness
Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
I didn’t run Rust tests here because the environment blocks Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewNow I have enough context to write a thorough review. PR Review: fix: drop proof-less Type-1 placeholders that broke block-proof mergeOverviewThis PR fixes a crash where The root-cause diagnosis is sound and the fix is logically correct. Issues1.
Bare keys do get pruned by 2. No unit tests for The new 3. Double When 4. Integration tests in #[doc(hidden)]
pub fn update_head(store: &mut Store, log_tree: bool) { ... }5. Removed The deleted comment ( Minor observations
SummaryThe core fix is correct and the spec alignment is well-justified. The most actionable items before merge are:
Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryThis PR fixes a Type-2 merge crash on devnet5 by removing the registration of block-attestation data into the known payload pool during block import. The root cause was that
Confidence Score: 5/5Safe to merge — the change removes a clearly broken code path and the replacement design is thoroughly reasoned. The deletion of the empty-proof pool seeding directly eliminates the crash site. The safety analysis in the PR description is rigorous: fork-choice weight derives from participant bits (not bare key presence), empty entries contributed zero merge input, and block-borne votes are still recovered through reaggregation/gossip within one slot. The test harness update correctly replicates the proposer-view store that fixtures require, going through No files require special attention.
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/store.rs | Removes the empty-proof pool seeding from on_block_core and exposes update_head as pub; both changes are correct and directly fix the Type-2 merge crash. |
| crates/blockchain/tests/forkchoice_spectests.rs | Test harness updated to structurally reconstruct per-attestation Type-1 entries from block body after a successful import and recompute the head, correctly reproducing the proposer-view store required by fixtures. |
| crates/common/types/src/block.rs | Removes a stale leanSpec PR reference from the MAX_ATTESTATIONS_DATA doc comment; no functional change. |
Sequence Diagram
sequenceDiagram
participant Net as Network
participant BC as on_block_core
participant KP as known_payloads
participant NP as new_payloads
participant FC as update_head
Note over BC,FC: Before this PR (broken path)
Net->>BC: SignedBlock (with attestations)
BC->>KP: insert empty-proof TypeOneMultiSig per attestation
BC->>FC: update_head()
FC->>KP: extract_latest_known_attestations()
Note over KP: empty proof bytes fed into merge_type_1s_into_type_2 CRASH
Note over BC,FC: After this PR (fixed path)
Net->>BC: SignedBlock (with attestations)
BC->>BC: count metrics only (no pool insert)
BC->>FC: update_head() block votes deferred
FC->>KP: extract_latest_known_attestations()
Note over KP,NP: Block votes recovered via reaggregation / gossip
Net-->>NP: gossip Type-1s or split_type_2_by_message
NP-->>KP: promote at next acceptance tick
FC->>KP: extract_latest_known_attestations() next slot
Reviews (2): Last reviewed commit: "docs: remove comment" | Re-trigger Greptile
on_block registered each block attestation's data in the known pool with an empty proof set, mirroring leanSpec's on_block. That registration is inert in our weight model: fork-choice weight is derived from proof participant bits, so an empty proof set contributes nothing, and the block builder skips entries with no proofs. Its only side effect was reserving an insertion-order slot, which reaggregation re-establishes anyway. The empty entries did, however, bypass the payload buffer's eviction cap: register_data inserted into the data map without incrementing total_proofs, so a non-finalizing chain could grow known_payloads unbounded. Remove the registration entirely. Block-borne votes were already zero-weight at import; weight still arrives once reaggregation recovers real Type-1 proofs and gossip delivers them. This eliminates the unbounded-growth path by construction and deletes the now-dead register_data helpers.
🤖 Kimi Code ReviewCritical Consensus Issue
If the SNARK-splitting logic is not yet implemented in the production import flow, this PR introduces a consensus-critical bug where imported block attestations are ignored by LMD GHOST, causing nodes to potentially settle on different heads than the spec expects. Verify that Code Quality & Safety
Documentation
Recommendations
Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code ReviewFindings
Notes I did not find additional issues in the small diff beyond that fork-choice regression. I also could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewNow I have all the context I need. Here is my review: Code Review:
|
Summary
Fixes the devnet error
Failed to merge Type-1s into Type-2 ... child proof deserialization failed at index N.Root cause: block import seeded the known payload pool with
TypeOneMultiSignature::empty(bits)placeholders (participant bits, empty proof bytes) so block-borne votes carried fork-choice weight. Block building read the same pool and fed those empty bytes into the Type-2 merge, where lean-multisig decompression failed.Fix: stop registering block-attestation data in the pool at import altogether.
A first pass mirrored leanSpec's
on_block— registering each attestation's data key with an empty proof set instead of a proof-less placeholder. That removed the crash, but the empty entries turned out to be inert in our weight model and introduced an unbounded-growth path (see below), so the final version drops the registration entirely.Why removing it is safe
ethlambda derives fork-choice weight from proof participant bits, not from data-key presence:
extract_latest_attestationsiteratesentry.proofs; an empty proof set yields zero voters.merge_type_1s_into_type_2.So a registered-but-empty entry contributed zero weight and zero merge input. Its only observable effect was reserving an insertion-order slot for same-slot equivocation tie-breaking, which reaggregation re-establishes in block-attestation order anyway.
Block-borne vote weight is still recovered: reaggregation SNARK-splits the block's merged Type-2 into per-attestation Type-1s (
split_type_2_by_message) into the new pool, and gossip delivers them; both migrate to the known pool at the next acceptance tick. This matches the spec's documented one-slot deferral.Bonus: closes an unbounded-growth path
The empty-proof entries bypassed the payload buffer's only capacity limit.
PayloadBufferevicts whentotal_proofs > capacity, but the bare-key insertion never incrementedtotal_proofs, so a non-finalizing chain could growknown_payloadspast its bound. Removing the registration eliminates this by construction (flagged by all four PR reviewers).Changes
crates/blockchain/src/store.rs:on_block_coreno longer registers block-attestation data keys (keeps the per-validator attestation-valid metric).crates/storage/src/store.rs: delete the now-deadPayloadBuffer::register_dataandStore::register_known_aggregated_data_batch.crates/blockchain/tests/forkchoice_spectests.rs: the harness deconstructs each imported block into per-attestation Type-1s (structurally, fromaggregation_bits) into the known pool and recomputes the head — reproducing the proposer-view store the fixtures encode, the role leanSpec's harness fills via its proposer-build simulation. These entries go throughpush, so they are properly accounted and bounded.Notes / follow-ups
newpool (deferred), which a leanSpec reviewer flagged as leaving receiving nodes with zero block-vote weight (Aggregated block proof - devnet5 leanEthereum/leanSpec#717, discussion_r3259006576). Thenew-vs-knownquestion remains open upstream.synced-gated inlib.rs), so block-borne votes during sync rely on gossip re-delivery. Accepted as an OK tradeoff for now; tracked as a follow-up.Test plan
cargo test --workspace --release— full suite passes (forkchoice 84, stf 51, signature 13, storage 38, ssz 118, …; 0 failures)cargo fmt --all -- --checkcleancargo clippy --workspace --release -- -D warningsclean