Summary
node/store_tick.go:47 calls PromoteNewToKnown() at interval 0 of a slot when the local node is the proposer. The leanSpec migration sequence puts new → known migration only at interval 4 (end of slot), strictly after the interval-3 safe-target computation.
case 0:
// Start of slot — promote attestations if proposal exists.
if shouldSignalProposal {
s.PromoteNewToKnown()
}
leanSpec PR #680 (leanEthereum/leanSpec#680) makes the interval-3 safe-target computation read the new pool only, on the explicit assumption that migration runs at interval 4. The docstring notes 3sf-mini is free to migrate earlier but does not, "precisely so that safe target sees only freshly received votes from the current slot."
Why it is real but narrow
The window where the interval-0 promotion does observable work is gossip that arrived between slot S-1 interval 4 (last drain) and slot S interval 0 (this drain). On the proposer node those late votes get pushed into known and are excluded from the slot-S safe-target view at interval 3. On non-proposer nodes those same votes stay in new and would be included. So proposer and non-proposer can disagree on safe target by exactly those late aggregations.
In practice this is rarely consequential — by interval 3 the new pool is repopulated from this slot's interval-2 aggregations and that is what matters for the threshold check. But the divergence is real and the spec ordering exists for a reason.
Suggested fix
Drop the interval-0 PromoteNewToKnown() call. The proposer already calls s.PromoteNewToKnown() immediately before buildBlock in ProduceBlockWithSignatures (node/store_build.go:41), which covers the proposer's own knowledge-freshness requirement. The interval-0 call is redundant for proposer correctness and breaks the spec's interval-3/interval-4 separation.
Context
Surfaced during the audit for the safe-target fix (gean PR for leanSpec #680 conformance). Out of scope for that PR; tracked separately here.
Summary
node/store_tick.go:47callsPromoteNewToKnown()at interval 0 of a slot when the local node is the proposer. The leanSpec migration sequence puts new → known migration only at interval 4 (end of slot), strictly after the interval-3 safe-target computation.leanSpec PR #680 (leanEthereum/leanSpec#680) makes the interval-3 safe-target computation read the new pool only, on the explicit assumption that migration runs at interval 4. The docstring notes 3sf-mini is free to migrate earlier but does not, "precisely so that safe target sees only freshly received votes from the current slot."
Why it is real but narrow
The window where the interval-0 promotion does observable work is gossip that arrived between slot S-1 interval 4 (last drain) and slot S interval 0 (this drain). On the proposer node those late votes get pushed into known and are excluded from the slot-S safe-target view at interval 3. On non-proposer nodes those same votes stay in new and would be included. So proposer and non-proposer can disagree on safe target by exactly those late aggregations.
In practice this is rarely consequential — by interval 3 the new pool is repopulated from this slot's interval-2 aggregations and that is what matters for the threshold check. But the divergence is real and the spec ordering exists for a reason.
Suggested fix
Drop the interval-0
PromoteNewToKnown()call. The proposer already callss.PromoteNewToKnown()immediately beforebuildBlockinProduceBlockWithSignatures(node/store_build.go:41), which covers the proposer's own knowledge-freshness requirement. The interval-0 call is redundant for proposer correctness and breaks the spec's interval-3/interval-4 separation.Context
Surfaced during the audit for the safe-target fix (gean PR for leanSpec #680 conformance). Out of scope for that PR; tracked separately here.