Skip to content

Integrate watermark into evmrpc#2465

Merged
jewei1997 merged 28 commits into
mainfrom
watermark-evmrpc
Oct 27, 2025
Merged

Integrate watermark into evmrpc#2465
jewei1997 merged 28 commits into
mainfrom
watermark-evmrpc

Conversation

@jewei1997

@jewei1997 jewei1997 commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

A common workflow that users do is to query the latest height and then query information about that height such as events/logs. However, since we have an option to do async writes for receipts, the events/logs may not be available yet for that height, causing the query to fail.

In general, we want all of our heights outputted by our RPC endpoints to be aligned with each other. We have introduced the concept of a watermark (sei-protocol/sei-db#116) across Tendermint block store, SS Store, and receipt store. Pulling this watermark across all 3 dbs should allow the evmrpc to output the minimum height available across these dbs for a latest height. This will ensure that all data for a given height outputted by our RPCs is available across all our endpoints.

This PR introduces a WatermarkManager that manages the heights of these DBs, which the evmrpc endpoints can use to get the latest heights that are ready.

Testing performed to validate your change

unit tests + manual testing

@github-actions

github-actions Bot commented Oct 13, 2025

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedOct 27, 2025, 6:57 PM

@codecov

codecov Bot commented Oct 13, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.90476% with 128 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.57%. Comparing base (555bd4c) to head (9c745f3).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
evmrpc/watermark_manager.go 72.46% 20 Missing and 18 partials ⚠️
evmrpc/filter.go 52.54% 17 Missing and 11 partials ⚠️
testutil/keeper/evm.go 0.00% 22 Missing ⚠️
x/evm/keeper/receipt.go 54.16% 8 Missing and 3 partials ⚠️
evmrpc/info.go 61.53% 5 Missing and 5 partials ⚠️
evmrpc/state.go 62.50% 3 Missing and 3 partials ⚠️
sei-cosmos/storev2/rootmulti/store.go 0.00% 5 Missing ⚠️
evmrpc/block.go 66.66% 3 Missing ⚠️
sei-db/ss/pebbledb/db.go 0.00% 1 Missing and 1 partial ⚠️
x/evm/keeper/keeper.go 0.00% 2 Missing ⚠️
... and 1 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2465      +/-   ##
==========================================
- Coverage   43.62%   43.57%   -0.05%     
==========================================
  Files        1679     1695      +16     
  Lines      143747   145352    +1605     
==========================================
+ Hits        62708    63338     +630     
- Misses      75414    76311     +897     
- Partials     5625     5703      +78     
Flag Coverage Δ
sei-chain 31.66% <63.22%> (+0.16%) ⬆️
sei-cosmos 52.36% <0.00%> (-0.04%) ⬇️
sei-db 48.02% <0.00%> (-0.02%) ⬇️
sei-tendermint 47.47% <ø> (-0.17%) ⬇️
sei-wasmd 45.80% <100.00%> (-0.03%) ⬇️
sei-wasmvm 40.37% <ø> (?)

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

Files with missing lines Coverage Δ
app/app.go 73.29% <100.00%> (+0.01%) ⬆️
app/test_helpers.go 47.19% <ø> (-1.63%) ⬇️
evmrpc/config.go 87.80% <ø> (+1.92%) ⬆️
evmrpc/server.go 91.09% <100.00%> (+0.23%) ⬆️
evmrpc/tx.go 85.31% <100.00%> (+0.75%) ⬆️
evmrpc/utils.go 71.16% <88.88%> (-2.76%) ⬇️
sei-db/ss/pebbledb/db.go 63.86% <0.00%> (-0.20%) ⬇️
x/evm/keeper/keeper.go 48.95% <0.00%> (-0.30%) ⬇️
evmrpc/block.go 54.60% <66.66%> (+3.18%) ⬆️
sei-cosmos/storev2/rootmulti/store.go 31.91% <0.00%> (-0.33%) ⬇️
... and 6 more

... and 34 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.

Comment on lines +117 to +121
for h := range c.mockedBlockResults {
if h > latest {
latest = h
}
}

Check warning

Code scanning / CodeQL

Iteration over map Warning test

Iteration over map may be a possible source of non-determinism
@jewei1997 jewei1997 changed the title [wip] Integrate watermark into evmrpc Integrate watermark into evmrpc Oct 14, 2025
Comment thread CHANGELOG.md
Comment thread evmrpc/block.go Outdated
Comment thread evmrpc/block.go Outdated
Comment thread evmrpc/config.go Outdated
Comment thread evmrpc/filter.go Outdated
Comment thread evmrpc/filter.go Outdated
Comment thread evmrpc/info.go
Comment thread evmrpc/watermark_manager.go Outdated
Comment thread evmrpc/watermark_manager.go Outdated
@yzang2019

Copy link
Copy Markdown
Contributor

We should be able to remove sync flush receipt in this PR since we have watermark

Comment thread evmrpc/watermark_manager.go Outdated
Comment thread x/evm/keeper/receipt.go
if err := k.FlushTransientReceipts(ctx); err != nil {
return err
}
deadline := time.Now().Add(2 * time.Second)

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism
Comment thread x/evm/keeper/receipt.go
} else if err != nil && err.Error() != "not found" {
return err
}
if time.Now().After(deadline) {

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism

@codchen codchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: for the earliest state, is it possible for it to become stale after the request to pass the check but before the state is read? (i.e. state pruned after the check but before the actual read)

@jewei1997 jewei1997 enabled auto-merge (squash) October 27, 2025 19:15
@jewei1997 jewei1997 merged commit abdeefe into main Oct 27, 2025
46 of 53 checks passed
@jewei1997 jewei1997 deleted the watermark-evmrpc branch October 27, 2025 19:32
@jewei1997

Copy link
Copy Markdown
Contributor Author

@codchen I think the situation you mentioned is possible. However, I think we mostly care about latest height for the watermark guarantees.

m3diumrare pushed a commit to m3diumrare/sei-chain that referenced this pull request Jun 9, 2026
…ei-protocol#3530)

## Summary

Several Ethereum JSON-RPC endpoints that take a block identifier
currently surface the watermark race (`"requested height N is not yet
available; safe latest is N-1"`) as an error, where the Ethereum
JSON-RPC spec says they should return JSON `null` (block doesn't exist
*yet* from the caller's perspective). Wallets, indexers, and similar
tools see this as a transient failure exactly when a tx has just been
mined and the safe-latest watermark hasn't caught up.

sei-protocol#3119 fixed `eth_getBlockByNumber` and sei-protocol#3501 fixed
`eth_getTransactionReceipt` with the same `if errors.Is(err,
ErrBlockHeightNotYetAvailable) { return nil, nil }` pattern. This PR
generalizes the pattern into two helpers and applies it to the remaining
user‑facing endpoints.

## Endpoints converted

| RPC method | File:line |
|---|---|
| `eth_getBlockReceipts` | `block.go` |
| `eth_getBlockTransactionCountByNumber` | `block.go` |
| `eth_getBlockTransactionCountByHash` | `block.go` |
| `eth_getBlockByHash` + `sei_getBlockByHashExcludeTraceFail` (shared
`getBlockByHash`) | `block.go` |
| `eth_getTransactionByBlockNumberAndIndex` | `tx.go` |
| `eth_getTransactionByBlockHashAndIndex` | `tx.go` |
| `eth_getTransactionByHash` | `tx.go` |

## Historic context (why propagation was the previous default)

The watermark integration (sei-protocol#2465) deliberately propagated the error so
clients would know "data not yet ready — retry later" rather than
getting a misleading null. That was the original conservative default
across **all** evmrpc endpoints that touched a height.

Since then, subsequent PRs have progressively chosen JSON `null` per
Ethereum spec on a per‑endpoint basis:

| PR | Endpoint | Rationale |
|---|---|---|
| sei-protocol#3067 (Moji) | `eth_getBlockByHash` (unknown hash) | PLT‑162: "return
result null for non‑existent hashes" |
| sei-protocol#3119 (Moji) | `eth_getBlockByNumber` (above watermark) | "Align with
expectations" (Ethereum spec) |
| sei-protocol#3320 (Wen) | `eth_getTransactionByBlock*AndIndex` (out‑of‑range
index) | "Per Ethereum JSON‑RPC spec, these methods must return null" |
| sei-protocol#3501 | `eth_getTransactionReceipt` (above watermark) | spec |
| **sei-protocol#3530** (this) | the **remaining 7** user‑facing endpoints |
continues the same pattern |

So the propagation on these 7 endpoints wasn't a deliberate "this
endpoint should error" choice — it's historical inertia from sei-protocol#2465 that
subsequent PRs have been chipping away one endpoint at a time. sei-protocol#3530
finishes the same job; nothing about the original watermark intent is
being reversed where it still serves users (see *out of scope* below).

## Out of scope

State queries (`eth_getBalance`/`Code`/`StorageAt`/`Proof`), simulation
paths, filter internals, and `info.go`/`association.go` helpers keep
using `blockByNumberRespectingWatermarks` directly. These are where the
original sei-protocol#2465 design still applies:

- The spec is fuzzier (some clients expect error for invalid blocks).
- The "data not ready" semantic is more meaningful — a wallet showing
`balance: 0` for a not‑yet‑ready block would mislead users.
- Internal call sites validate heights and rely on the error path.

Tests covering those error paths
(`TestStateAPIGetProofUnavailableHeight`, the filter‑cache test) are
intentionally unchanged.

**Pruned-block path also unchanged.** Explicit numeric requests for
heights below the earliest watermark (`height < earliest`) still return
`"requested height N has been pruned"`. The helpers only convert
`ErrBlockHeightNotYetAvailable` (and `ErrBlockNotFoundByHash` for the
by-hash variant). Pre-PR behavior is preserved here — pruned is a
different (permanent, not transient) failure class from the watermark
race this PR addresses, and changing it would touch a different set of
test assertions (`watermark_manager_test.go:106,135`). Hash-based
requests for pruned blocks already return null in production because
Tendermint returns `Block: nil` for unknown hashes, which
`blockByHashWithRetry` wraps as `ErrBlockNotFoundByHash`.

## Helpers

```go
// blockByNumberOrNullForJSONRPC: wraps blockByNumberRespectingWatermarks
// and converts ErrBlockHeightNotYetAvailable to (nil, nil) so callers can
// return JSON null per the Ethereum spec.

// blockByHashOrNullForJSONRPC: same, by-hash variant. Also catches
// ErrBlockNotFoundByHash — both are "block doesn't exist" per spec.
```

Internal call sites that want the error keep using the originals.

## Operational impact (broader than tests)

This is a production-correctness fix. Currently a wallet polling for a
freshly-mined tx, or an indexer doing `eth_getBlockReceipts` near the
chain head, can intermittently get a `"block height not yet available"`
error response instead of `null` — they then surface that as a transient
failure to users. With this change, those endpoints behave per Ethereum
JSON-RPC spec.

## Test plan

- [x] `go test ./evmrpc/ -count=1` passes (21s, includes the updated
above-watermark tests).
- [x] `golangci-lint run ./evmrpc/`: 0 issues.
- [x] `gofmt -s -l` clean.
- [ ] CI integration matrix passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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