Conversation
## 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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## giga-1 #4189 +/- ##
==========================================
- Coverage 65.55% 65.46% -0.10%
==========================================
Files 2081 2078 -3
Lines 157460 157301 -159
==========================================
- Hits 103222 102975 -247
- Misses 54097 54185 +88
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
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: |
PR SummaryHigh Risk Overview Receipt store (littidx) applies writes on a bounded background queue with depth/backpressure metrics ( Giga storage disables the EVM state store’s internal Pebble changelog when opened via EVM-only RPC adds Gigasim / Grafana pre-marshals receipts on the block generator, removes encode-vs-store receipt metrics, and reshuffles execution drill-down panels (receipt write vs litt queues, SS commit queue blocked time, etc.). Reviewed by Cursor Bugbot for commit a5ab204. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
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-1but mergesmaininto the branch, so 35 of the 39 changed files are unrelated already-merged work (#4155, #4140, #4159) that will land ongiga-1with this merge. Worth confirming that is intended, since the only change authored here is thesei-tendermint/internal/autobahn/availcommit. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
|
Re #4189 (comment): went with the full fix rather than a TODO, in 72f4c0a. |
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.
Reviewed by Cursor Bugbot for commit a5ab204. Configure here.
| if b.Number < q.next { | ||
| if b.Number == q.next-1 { | ||
| q.lastHash = utils.Some(b.Proposal.Msg().Block().Header().Hash()) | ||
| } |
There was a problem hiding this comment.
Restart parent hash misses WAL gaps
Medium Severity
restoreBlocks only copies lastHash from a skipped WAL block whose number is exactly q.next-1. truncateForAnchor can keep a later-than-anchor last block and jump nextBlockNum to first, so after restart the retained block sits below q.next-1 and lastHash stays unset. The producer then emits a zero parent again. That in-memory cursor jump is also not durable, so loadAll rebuilds nextBlockNum from the kept block and a later persist can fail the sequence check.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a5ab204. Configure here.


ProduceLocalBlockderived 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, soPushBlockcompared the zero hash against the real one, loggedparent hash mismatch (producer equivocation)and dropped the block. Later blocks of that lane then parked inPushBlock'sWaitUntiluntil 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
blockQueuewhich records the hash of the last block pushed, andProduceLocalBlockuses 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:
restoreInnerprunes to the Anchor before replaying the lane WAL, andtruncateForAnchorcould 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))), andrestoreBlocksseedslastHashfrom the skipped WAL block atq.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_ParentHashSurvivesRestartandTestPrunePastAllKeepsLastBlockeach fail without the corresponding change and pass here.