Retain Autobahn lane last block across prune - #4199
Conversation
…ot stall on a zero parent hash.
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4199 +/- ##
==========================================
- Coverage 66.70% 65.57% -1.14%
==========================================
Files 2204 2083 -121
Lines 169825 157898 -11927
==========================================
- Hits 113279 103534 -9745
+ Misses 56405 54223 -2182
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ff8b76c. Configure here.
There was a problem hiding this comment.
Retaining the last lane proposal across a prune correctly fixes the zero-parent stall and the setNextBlockToPersist rewind, and the last/retentionFloor/unpersistedLast invariants hold up (last is always either the queue tip or exactly first-1, so the persist batch stays contiguous). However, enforcing the parent-hash check against the retained last removes the previously deliberate recovery path at the prune boundary, so a node holding an equivocating predecessor can no longer resync onto the certified chain.
Findings: 1 blocking | 0 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
- 1 blocking issue(s) flagged inline on specific lines.
Non-blocking
- None at the file/PR level.
|
Relates to #4189 |
The proposed fix is wrong, local last block and qc block might be on different block number.
We plan to do reconcilation of local lasthash and qc block hash in the future.
|
@seidroid re-review please |
|
Claude finished @wen-coding's task in 6m 8s —— View job Re-review —
|
…TODO on the clear.
|
@seidroid review |
There was a problem hiding this comment.
Retaining the last lane proposal across a prune correctly fixes the zero-parent-hash stall, and the reworked check now skips the parent comparison outside [first, next) so the prune-boundary recovery path from the previous review is restored. The new retentionFloor/unpersistedLast/no-rewind invariants hold up under the orderings I traced; remaining notes are about deleted rationale and maintaining last for lanes that never read it.
Findings: 0 blocking | 2 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- None at the file/PR level.
- 2 suggestion(s)/nit(s) flagged inline on specific lines.
| // NOTE: after pruning (q.first >= q.next), we cannot verify the parent | ||
| // hash because the previous block is gone. This is safe because | ||
| // headers() never follows the first block's parentHash in a LaneRange. | ||
| // Parent is checked only while the predecessor is still in [first, next). |
There was a problem hiding this comment.
[suggestion] The replacement comment states the scope of the check but drops the invariant that made the gap safe: headers() seeds want from lr.LastHash() and walks backwards (state.go:541), and FullCommitQC.Verify does the same (types/commit_qc.go:99-103), so the first block's parentHash in a LaneRange is never followed. That is the reason an unverified parent at the prune boundary cannot deadlock header reconstruction, and it is now recorded nowhere.
I confirmed the invariant still holds on the current tree, so this is documentation rather than behaviour — but per AGENTS.md ("Relocating a load-bearing invariant is the move, never deleting one to tidy up") it should be carried over rather than removed. Without it, the next reader has no way to tell whether if q.first < q.next is a deliberate, safe narrowing or an oversight — which is exactly the question the previous round of review had to re-derive from scratch.
The same applies to the sibling edit at inner.go:146, which dropped the note that the WAL prunes lazily and can therefore hand restoreBlocks blocks below the anchor. That reason is now more load-bearing than before, since this PR deliberately retains one such block via retentionFloor.
| // blockQueue is a per-lane block queue. | ||
| type blockQueue struct { | ||
| queue[types.BlockNumber, *types.Signed[*types.LaneProposal]] | ||
| // last is None, or this node's last pushed proposal at height >= first-1. |
There was a problem hiding this comment.
[suggestion] last is only ever read for local block production (ProduceLocalBlock, state.go:664) — PushBlock deliberately does not consult it — yet pushBack sets it for every lane, so retentionFloor and unpersistedLast keep and may flush one extra signed proposal (full payload) per foreign lane, in memory and in the WAL, with no consumer. On a large committee that is one retained proposal per validator held past the anchor for the life of the process.
Related: the doc comment says "this node's last pushed proposal", but PushBlock sets it from a remote producer's block too. A reader could reasonably conclude foreign lanes don't retain anything here.
If gating is wanted, the choke point is addLane/newBlockQueue (which is where the local key could be compared against lane.Validator) rather than a condition at each pushBack caller. Otherwise, at minimum reword the comment to match what the code does.
|
Successfully created backport PR for |

Summary
[first, next), keep the last local lane proposal so the next produced/pushed block still names a real parent instead of the zero hash (which stalled the lane).nextBlockToPersist.Nextpast the local tip / empty-range zeroLastHash) is left as a TODO; this change only covers last-seen local production.Test plan
GOWORK=off go test ./sei-tendermint/internal/autobahn/avail/ -count=1Made with Cursor