Skip to content

Keep lane parent hash across pruning in ProduceLocalBlock - #4189

Open
masih wants to merge 10 commits into
giga-1from
masih/1789482635-lane-parent-hash-after-prune
Open

masih wants to merge 10 commits into
giga-1from
masih/1789482635-lane-parent-hash-after-prune

Conversation

@masih

@masih masih commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

ProduceLocalBlock derived the parent hash from the lane queue, and emitted the zero hash whenever the queue had been fully pruned by the data Anchor (the lane idle, every produced block already certified and evicted). A replica whose Anchor had not advanced as far still held the previous block, so PushBlock compared the zero hash against the real one, logged parent hash mismatch (producer equivocation) and dropped the block. Later blocks of that lane then parked in PushBlock's WaitUntil until the replica's own Anchor pruned past them, so the lane froze until the next commit round caught up. On brandon-autobahn-07 (running 3cad9bf) this fired on every producer lane and showed up as the global block number advancing in stall-then-burst steps, create-to-execute latency p50 of 60-115s, and a low blocks/sec on the autobahn-e2e dashboard, while commit-to-commit latency itself stayed at ~0.12s.

The lane block queue is now a blockQueue which records the hash of the last block pushed, and ProduceLocalBlock uses that instead of reading the previous entry, so pruning no longer changes the parent a producer puts on its next block. The receiver check is unchanged.

The same gap existed across a restart: restoreInner prunes to the Anchor before replaying the lane WAL, and truncateForAnchor could drop every block of an idle lane, so a node restarting with a fully certified lane produced its first block with a zero parent again. The lane WAL now always retains its last persisted block (PruneBefore(min(first, nextBlockNum-1))), and restoreBlocks seeds lastHash from the skipped WAL block at q.next-1. LaneRange.LastHash() from the Anchor is deliberately not used as a source, since it is zero for a lane the tipcut omitted.

Tested with go test -race ./internal/autobahn/... in sei-tendermint. TestProduceLocalBlock_ParentHashSurvivesPrune, TestProduceLocalBlock_ParentHashSurvivesRestart and TestPrunePastAllKeepsLastBlock each fail without the corresponding change and pass here.

cody-littley and others added 4 commits September 15, 2026 12:02
## Describe your changes and provide context

Move account read/fold workflow off of the execution thread.

---------

Co-authored-by: Cody Littley <cody.littley@seinetwork.io>
## Describe your changes and provide context

- expose `eth_getBalance` from the EVM-only JSON-RPC server
- read balances from the current committed EVM state
- support `latest`, `safe`, `finalized`, and `pending`, while rejecting
block heights and hashes until historical state is wired
- add unit, JSON-RPC registration, and Docker integration coverage
- document the endpoint and its supported block selectors in the
Autobahn README

## Testing performed to validate your change

- `go test -race -count=1 ./giga/evmonly/rpc/...`
- `go test -count=1 ./sei-tendermint/internal/rpc/core/...`
- `make autobahn-evmonly-integration-test` (four local Docker
validators, 4,000 finalized transfers, post-transfer balance checks on
every validator)
- `golangci-lint run ./giga/evmonly/rpc/...
./sei-tendermint/internal/rpc/core/...`
- `golangci-lint fmt --diff`
## Describe your changes and provide context

Both the receipt write and the EVM state store (SS) write were doing
slow synchronous work on the block commit path. This makes both of them
fully async.

### Receipt store

`SetReceipts` used to write the receipt bodies, the `eth_getLogs` index
and the version marker inline, and the index commit alone was ~74% of
the call. It now hands the block to a background writer and returns.

Measured at 2,000 receipts per block, the commit path went from **4.0 ms
to 11 µs**. The work still costs the same; it just happens on the
writer, where it overlaps with execution instead of serializing against
it.

- `receipt-store.async-write-buffer` (default 100) bounds how many
blocks the store may fall behind. A full queue blocks the caller — that
is the back-pressure.
- Setting it to `<= 0` keeps writes synchronous, which is the escape
hatch if strict read-after-write is wanted.
- `LatestVersion()` only advances once a write has actually been
applied, so it never advertises a receipt that is not yet readable. It
is the watermark a reader follows.

### EVM state store

`enqueue_ss` looked async but was dominated by a **synchronous changelog
WAL write sitting in front of the queue**. That is also why its queue
depth always read 0: queue depth only reveals a slow consumer, and here
the producer was the slow side.

Under giga that changelog is written every block and never read — crash
recovery replays giga's own state WAL via `catchUpTo`, and rollback
rewinds SS from its snapshots against that same WAL. So giga now opens
SS with `DisableInternalWAL` and the commit-path write is gone.

The composite (non-giga) path is untouched and keeps its changelog,
which it does need: `ss/composite` rollback replays it to reach versions
above a snapshot.

### Interface cleanup

`SetLatestVersion` / `SetEarliestVersion` are no longer on the
`ReceiptStore` interface. No production code called them — the write
path carries the markers, and every external caller was test or
benchmark scaffolding. cryptosim's redundant `SetLatestVersion` after
each block is deleted for the same reason.

### Bug fixed along the way

Draining the pebble async writer on close was nested inside the
changelog check:

```go
if db.streamHandler != nil {
    close(db.pendingChanges)
    db.asyncWriteWG.Wait()
    ...
}
```

With the changelog off, that drain would never run, silently dropping
queued blocks on every clean shutdown. The drain is now unconditional,
behind a `sync.Once` so `Close` stays idempotent.

### Dashboard

`receipt_write_queue_depth` now covers the whole receipt write. The old
"ReceiptDB Queue Depth" panel tracked only litt's table queue, which is
~7% of the call, which is why it read 0 while `write_receipts` was a
large share of the execution loop.

## Testing performed to validate your change

- `sei-db/ledger_db/...`, `sei-db/state_db/...`, `sei-db/bootstrap`,
`sei-db/config`, `sei-db/db_engine/pebbledb/...`, `giga/evmonly/...`,
`evmrpc/...` and `x/evm/keeper` all pass.
- The receipt package passes three repeats under `-race`.
- `make dblint` reports 0 issues; `go vet ./...` is clean.

New tests:

- `TestLittIdxSynchronousWriteBuffer` — with the buffer off, a block is
queryable the moment `SetReceipts` returns.
- `TestLittIdxWriteBufferBoundsLag` — the buffer is the back-pressure
point; the store cannot trail further than it allows.
- `TestOpenSSKeepsNoChangelogOfItsOwn` — pins the absence of the SS
changelog under giga rather than trusting the config. Verified
non-vacuous by re-enabling the flag and watching it fail.

Tests that previously relied on read-after-write now wait on
`LatestVersion` instead. Worth noting for reviewers: that watermark is
necessary but not sufficient as a "my write landed" signal — the bodies
land just before the version marker commits, and a block written in
parts advances the marker on its first part. The `littidx` helper waits
on both.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 15, 2026, 9:20 PM

@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.25843% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.46%. Comparing base (7e3ca24) to head (e4ec60b).
⚠️ Report is 5 commits behind head on giga-1.

Files with missing lines Patch % Lines
sei-db/ledger_db/receipt/receipt_store.go 66.66% 2 Missing ⚠️
sei-db/state_db/giga/state_db.go 50.00% 2 Missing ⚠️
sei-db/db_engine/pebbledb/mvcc/db.go 91.66% 1 Missing ⚠️
...mint/internal/autobahn/consensus/persist/blocks.go 80.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           giga-1    #4189      +/-   ##
==========================================
- Coverage   65.55%   65.46%   -0.10%     
==========================================
  Files        2081     2077       -4     
  Lines      157460   157257     -203     
==========================================
- Hits       103222   102942     -280     
- Misses      54097    54174      +77     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 78.85% <95.29%> (?)
sei-db ?
sei-db-state-db-pr 30.27% <50.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
evmrpc/tests/utils.go 79.62% <100.00%> (-0.25%) ⬇️
sei-db/config/giga_config.go 81.96% <100.00%> (+0.30%) ⬆️
sei-db/config/receipt_config.go 85.71% <100.00%> (ø)
sei-db/config/ss_config.go 100.00% <ø> (ø)
sei-db/ledger_db/receipt/litt_receipt_store.go 86.34% <100.00%> (+2.88%) ⬆️
sei-tendermint/internal/autobahn/avail/inner.go 96.77% <100.00%> (+1.08%) ⬆️
sei-tendermint/internal/autobahn/avail/state.go 85.78% <100.00%> (-0.30%) ⬇️
sei-db/db_engine/pebbledb/mvcc/db.go 77.71% <91.66%> (+0.06%) ⬆️
...mint/internal/autobahn/consensus/persist/blocks.go 80.00% <80.00%> (+0.42%) ⬆️
sei-db/ledger_db/receipt/receipt_store.go 72.88% <66.66%> (-0.22%) ⬇️
... and 1 more

... and 62 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@masih
masih changed the base branch from main to giga-1 September 15, 2026 15:02
@wen-coding

Copy link
Copy Markdown
Contributor

The in-process fix is right: lastHash on pushBack, ProduceLocalBlock reads that, prune does not clear it. But I think let's add a TODO to fix restart case:
Restart: lastHash is not restored when the queue is empty after prune (WAL entries below q.next are skipped). Do not seed it from the current QC’s LaneRange.LastHash(); that is zero when the tipcut omitted the lane even if the lane has history. Keep the last header (or lastHash) across prune/WAL truncate instead.

@masih
masih marked this pull request as ready for review September 15, 2026 15:03
@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes touch consensus block production after pruning/restart and move receipt persistence to a background queue with a smaller default buffer, both of which affect liveness, recovery, and RPC read windows; dashboard-only edits are low risk but bundled with those paths.

Overview
Autobahn lane production no longer derives the next block’s parent hash from the in-memory queue (which went empty after Anchor pruning). A blockQueue remembers the last pushed block hash, the lane WAL keeps the last persisted block when pruning past the anchor, and restore seeds that hash so ProduceLocalBlock stays consistent across prune and restart—addressing dropped blocks and lane stalls when replicas lag on Anchor.

Receipt store (littidx) applies SetReceipts through an optional async queue (AsyncWriteBuffer, default 10 instead of 100), with back-pressure metrics, latched write failures, and Close draining the queue. Version markers move behind a VersionPinner / receipt.PinVersions helper; EVM RPC and receipt tests pin query windows instead of calling SetLatestVersion directly. Giga disables the state store’s internal Pebble changelog WAL (replay comes from the state WAL) and pre-marshals receipt records in the block generator while dropping gigasim “encode vs store” phase timers.

Gigasim Grafana dashboard retitles/requeries receipt queue depth (receipt_write_queue_depth plus litt control/flush), removes the old write-receipts encode pie, shifts several per-block phase panels, adds receipt-store litt vs log-index breakdown and SS commit queue blocked time, and places the new backpressure panel on the grid.

Reviewed by Cursor Bugbot for commit e4ec60b. Bugbot is set up for automated code reviews on this repo. Configure here.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blockQueue/lastHash change correctly fixes the zero-parent-hash stall for a running node, and the new test pins it. One residual gap: the restart path never repopulates lastHash when the anchor has already advanced past every WAL block, so the same stall can recur after a restart of an idle lane.

Findings: 0 blocking | 2 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The PR targets giga-1 but merges main into the branch, so 35 of the 39 changed files are unrelated already-merged work (#4155, #4140, #4159) that will land on giga-1 with this merge. Worth confirming that is intended, since the only change authored here is the sei-tendermint/internal/autobahn/avail commit.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-tendermint/internal/autobahn/avail/inner.go
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Re #4189 (comment): went with the full fix rather than a TODO, in 72f4c0a. truncateForAnchor now keeps the lane's last persisted block on disk even when the anchor has moved past it, and restoreBlocks seeds lastHash from the skipped WAL block at q.next-1. LaneRange.LastHash() is not consulted. TestProduceLocalBlock_ParentHashSurvivesRestart (certify the lane's only block, restart, produce) fails with a zero parent before this and passes now; TestPrunePastAllKeepsLastBlock covers the WAL retention.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a5ab204. Configure here.

Comment thread sei-tendermint/internal/autobahn/avail/inner.go
@wen-coding

wen-coding commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Okay, thought about it further, three things:

  1. The saved lastHash should be preserved with block number
  2. If there is a parent b in local but you see b' in a QC, you should favor b' over b, and you should also discard whatever disagrees locally and refetch (this part we probably have never implemented)
  3. If a lane has no blocks in a QC, we should copy over lastHash from last QC so we don't lose the lastHash

I feel there is a regression somewhere in PR #3849, I need to take a closer look tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants