Summary
The spec permits a block whose proof cannot be built. A block may carry several distinct AttestationData for the same slot, but a leanVM aggregate carries at most one message per epoch, and the epoch is the slot. The proposer then fails to produce its block proof and the slot is lost.
Two of three clients on a mixed devnet are losing slots to this today.
Where the spec stands
The block rules constrain the number of distinct data and forbid repeats, but say nothing about two different data sharing a slot:
spec/forks/lstar/config.py:58 — MAX_ATTESTATIONS_DATA = 8
spec/forks/lstar/state_transition.py:218-221 — rejects a block whose distinct data exceed the cap (TOO_MANY_ATTESTATION_DATA)
spec/forks/lstar/fork_choice.py:619 — rejects a block that repeats an AttestationData (DUPLICATE_ATTESTATION_DATA)
spec/forks/lstar/block_production.py:129 — candidates are ordered by target slot, with the cap applied at line 148; nothing restricts two candidates that share data.slot
Validators disagreeing within a slot is ordinary: some have seen the slot's block and some have not, so they vote for different heads, or different targets, at the same slot. Each such vote is a distinct, valid AttestationData at one slot, and the spec invites a proposer to include them all.
Where the proof system stands
leanVM 022ec377:
crates/rec_aggregation/src/aggregation.rs check_signer_set requires strictly increasing epochs across XMSS claim groups, so one message per epoch
plan_coverage keeps "the epoch-to-message function every contributor must agree on" and keys XMSS claims as (epoch, key), with the message derived from the epoch
- a second, different message at one epoch returns
AggregationError::ConflictingMessages
The proposer's own signature occupies the block's own slot, so an attestation for that slot collides with it too.
Observed
Three-client devnet (gean, ethlambda, lantern), 14 validators, 4 committees, all on leanVM 022ec377, 8 hours:
| client |
slots lost to this |
| lantern |
186 (leanvm: merge_many_type_1 failed: two claims at one epoch carry different messages) |
| ethlambda |
41 (failed to merge single-message aggregates into a multi-message aggregate: prover failure: two claims at one epoch carry different messages) |
| gean |
0 — it keeps one AttestationData per slot, a rule it added itself |
Traced example: two aggregates for slot 86 with targets 82 and 83 arrived, both went into the block for slot 87, and sealing failed. Reported as lambdaclass/ethlambda#617 and bitminetech/lantern#196.
What should the rule be?
The spec needs to settle what a block may carry, so every client filters identically. Today it is silent, so each client decides for itself: gean keeps one AttestationData per slot, ethlambda and lantern do not, and they lose slots for it.
The shape we implemented in gean, offered as a starting point rather than a conclusion:
- a block carries at most one
AttestationData per slot, and none at the block's own slot
build_block (block_production.py) selects accordingly
- block validation says whether a block breaking the rule is invalid, or valid but unprovable
Note that filtering has a cost of its own: the dropped votes are honest, and they are exactly the votes cast when validators disagree, so justification slows at the moment the network most needs it. If that cost is unacceptable, the alternative is on the leanVM side, below.
Longer term
The restriction is stricter than XMSS requires. The signature rule is one message per key per epoch; leanVM enforces one message per epoch across all keys. SPHINCS in the same file keys claims on (key, message) ("one key signing two messages is two claims") while XMSS derives the message from the epoch. Whether that can be relaxed is a question for leanVM, raised separately; if it is relaxed, this spec rule can be dropped again.
Summary
The spec permits a block whose proof cannot be built. A block may carry several distinct
AttestationDatafor the same slot, but a leanVM aggregate carries at most one message per epoch, and the epoch is the slot. The proposer then fails to produce its block proof and the slot is lost.Two of three clients on a mixed devnet are losing slots to this today.
Where the spec stands
The block rules constrain the number of distinct data and forbid repeats, but say nothing about two different data sharing a slot:
spec/forks/lstar/config.py:58—MAX_ATTESTATIONS_DATA = 8spec/forks/lstar/state_transition.py:218-221— rejects a block whose distinct data exceed the cap (TOO_MANY_ATTESTATION_DATA)spec/forks/lstar/fork_choice.py:619— rejects a block that repeats anAttestationData(DUPLICATE_ATTESTATION_DATA)spec/forks/lstar/block_production.py:129— candidates are ordered by target slot, with the cap applied at line 148; nothing restricts two candidates that sharedata.slotValidators disagreeing within a slot is ordinary: some have seen the slot's block and some have not, so they vote for different heads, or different targets, at the same slot. Each such vote is a distinct, valid
AttestationDataat one slot, and the spec invites a proposer to include them all.Where the proof system stands
leanVM
022ec377:crates/rec_aggregation/src/aggregation.rscheck_signer_setrequires strictly increasing epochs across XMSS claim groups, so one message per epochplan_coveragekeeps "the epoch-to-message function every contributor must agree on" and keys XMSS claims as(epoch, key), with the message derived from the epochAggregationError::ConflictingMessagesThe proposer's own signature occupies the block's own slot, so an attestation for that slot collides with it too.
Observed
Three-client devnet (gean, ethlambda, lantern), 14 validators, 4 committees, all on leanVM
022ec377, 8 hours:leanvm: merge_many_type_1 failed: two claims at one epoch carry different messages)failed to merge single-message aggregates into a multi-message aggregate: prover failure: two claims at one epoch carry different messages)AttestationDataper slot, a rule it added itselfTraced example: two aggregates for slot 86 with targets 82 and 83 arrived, both went into the block for slot 87, and sealing failed. Reported as lambdaclass/ethlambda#617 and bitminetech/lantern#196.
What should the rule be?
The spec needs to settle what a block may carry, so every client filters identically. Today it is silent, so each client decides for itself: gean keeps one
AttestationDataper slot, ethlambda and lantern do not, and they lose slots for it.The shape we implemented in gean, offered as a starting point rather than a conclusion:
AttestationDataper slot, and none at the block's own slotbuild_block(block_production.py) selects accordinglyNote that filtering has a cost of its own: the dropped votes are honest, and they are exactly the votes cast when validators disagree, so justification slows at the moment the network most needs it. If that cost is unacceptable, the alternative is on the leanVM side, below.
Longer term
The restriction is stricter than XMSS requires. The signature rule is one message per key per epoch; leanVM enforces one message per epoch across all keys. SPHINCS in the same file keys claims on
(key, message)("one key signing two messages is two claims") while XMSS derives the message from the epoch. Whether that can be relaxed is a question for leanVM, raised separately; if it is relaxed, this spec rule can be dropped again.