Commit 2d5b273
Enforce single AttestationData per block (leanSpec PR #510) (#258)
## Motivation
Currently, `build_block` can produce multiple attestation entries
sharing the same `AttestationData` -- each backed by a different
aggregated signature proof. This happens when `extend_proofs_greedily`
selects multiple proofs for the same vote data (e.g., from different
aggregation intervals covering non-overlapping validator sets).
[leanSpec PR #510](leanEthereum/leanSpec#510)
introduces a new protocol invariant: **each unique `AttestationData`
must appear at most once per block**. Multiple proofs for the same vote
are compacted into a single entry, reducing block size and simplifying
downstream processing. The invariant is enforced on both the building
and validation sides.
## Description
### Block validation (`on_block_core`)
A cheap O(n) uniqueness check is inserted **before** signature
verification and state transition (the two most expensive steps). If a
block contains duplicate `AttestationData` entries, it is rejected with
a new `StoreError::DuplicateAttestationData` error. This uses
`HashSet<&AttestationData>` -- possible because `AttestationData`
already derives `Hash + Eq`.
### Block building (`build_block`)
After the existing greedy proof selection loop, a new
`compact_attestations` step groups all `(attestation, proof)` pairs by
their `AttestationData` and merges duplicates:
- **Single entry per data**: kept as-is (fast path).
- **Multiple entries with empty proofs** (skip-sig / devnet mode):
participant bitfields are unioned via `union_aggregation_bits`,
producing a single `AggregatedSignatureProof::empty(merged_bits)`.
- **Multiple entries with real proofs** (production with XMSS
aggregation): the proof covering the most participants is kept. Full
merge would require recursive proof aggregation, which lean-multisig
does not yet support (the [spec itself
notes](https://github.com/leanEthereum/leanSpec/blob/main/src/lean_spec/subspecs/xmss/aggregation.py#L72)
"The API supports recursive aggregation but the bindings currently do
not").
The intermediate block built inside the justification-check loop is
**not** compacted -- it only tests whether justification advances, and
vote counting is identical regardless of entry layout.
### New helpers
| Function | Purpose |
|----------|---------|
| `union_aggregation_bits(a, b)` | Bitwise OR of two `AggregationBits`
bitfields |
| `compact_attestations(atts, proofs)` | Groups by `AttestationData`,
merges duplicates, preserves first-occurrence order |
### Future work
When lean-multisig adds recursive aggregation support, the `else` branch
in `compact_attestations` (real proofs) can be upgraded to
cryptographically merge all proofs instead of keeping only the best one.
This will recover the small amount of validator coverage currently lost
when multiple real proofs exist for the same `AttestationData`.
## Test plan
- [x] `compact_attestations_no_duplicates` -- distinct data passes
through unchanged
- [x] `compact_attestations_merges_empty_proofs` -- two entries with
same data + empty proofs merge into one with unioned participants
covering all validators
- [x] `compact_attestations_real_proofs_keeps_best` -- two entries with
same data + real proofs keeps the one with more participants
- [x] `compact_attestations_preserves_order` -- multiple data entries
(some duplicated) output in first-occurrence order
- [x] `on_block_rejects_duplicate_attestation_data` -- block with
duplicate entries returns `DuplicateAttestationData` error via
`on_block_without_verification`
- [x] All 18 existing blockchain lib tests still pass
- [x] `cargo fmt --all -- --check` clean
- [x] `cargo clippy -p ethlambda-blockchain -- -D warnings` clean
- [ ] Spec test fixtures update (deferred until leanSpec submodule
includes PR #510)
---------
Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>1 parent 9a86501 commit 2d5b273
File tree
9 files changed
+484
-30
lines changed- crates
- blockchain/src
- common/types
- src
- net/p2p/src
- req_resp
9 files changed
+484
-30
lines changedSome generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
105 | 123 | | |
106 | 124 | | |
107 | 125 | | |
| |||
0 commit comments