Skip to content

Expose block time in milliseconds on the EVM RPC - #4018

Open
alexander-sei wants to merge 4 commits into
mainfrom
evmrpc-block-timestamp-ms
Open

Expose block time in milliseconds on the EVM RPC#4018
alexander-sei wants to merge 4 commits into
mainfrom
evmrpc-block-timestamp-ms

Conversation

@alexander-sei

@alexander-sei alexander-sei commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Sei commits blocks faster than once a second, so the Ethereum-standard timestamp — whole seconds — repeats across consecutive blocks and cannot tell them apart. The Tendermint header already carries the full precision (time.Time, seconds + nanos); the RPC layer was discarding it at serialization with .Unix().

This adds a Sei-only timestampMs (hex quantity, Unix milliseconds) alongside timestamp, which stays in seconds because every Ethereum client reads it that way. Emitted by all four block-header encoders, so a header from any of them carries it:

  • EncodeTmBlocketh_getBlockByNumber / eth_getBlockByHash
  • encodeGenesisBlock — the synthetic genesis block
  • encodeCommittedBlocketh_subscribe("newHeads"), Autobahn
  • encodeTmHeadereth_subscribe("newHeads"), CometBFT

Serialization only. No consensus, storage, or execution change. The seconds-based values feeding fork rules and the TIMESTAMP opcode (vm.BlockContext.Time, MakeSigner, ethtypes.Header.Time) are deliberately untouched — passing milliseconds there would read as a timestamp ~1000x in the future.

Notes for reviewers

  • Additive, so historical responses keep their existing fields and values. timestamp is unchanged for every height, which keeps the evmrpc/AGENTS.md consistency rule (responses for historical heights must not change as the chain progresses).
  • timestampMs is not unique per block. Under Autobahn a proposal spaces consecutive blocks by 1µs (minTimestampDiff in sei-tendermint/autobahn/types/proposal.go), so blocks less than a millisecond apart tie. It is a large improvement over seconds, not a per-block identifier. Documented in evmrpc/AGENTS.md, and the chain assertion is written as non-decreasing rather than strictly increasing.
  • The geth-parity specs needed SEI_ONLY_BLOCK_FIELDS updated — they assert that any Sei block key absent from geth is on that allowlist. timestampMs was deliberately not added to CORE_BLOCK_FIELDS or assertCanonicalHeader, since both also run against geth blocks; Sei-only coverage goes through a new assertSeiTimestampMs helper.
  • The two newHeads fixtures were given a fractional-second time on purpose. With a whole-second fixture the new assertions would still pass if the field were implemented as timestamp * 1000, so they would not have been testing anything.

Test plan

  • go test ./evmrpc/... — passes
  • make fmtcheck — clean
  • go vet ./evmrpc/... ./integration_test/evm_module/ws_test/ — clean
  • New/updated unit coverage:
    • TestEncodeTmBlockTimestampMs — sub-second header time survives into timestampMs while timestamp truncates
    • TestEncodeCommittedBlock — exact value on the Autobahn newHeads encoder
    • TestSubscribeNewHeads — the two fields agree on the CometBFT newHeads path
    • TestSubscribeNewHeadsAutobahn — exact value over the real WS JSON transport
    • TestGetBlockTimestampMs (evmrpc/tests) — over the real HTTP JSON-RPC transport, including genesis
  • Integration suites not run locally (require a running node + geth reference): integration_test/rpc_tests (eth_getBlockByNumber, eth_getBlockByHash, eth_subscribe) and integration_test/evm_module/ws_test
  • TypeScript not typechecked locally — integration_test/rpc_tests/node_modules is not installed, so those edits are reviewed by hand rather than compiler-verified

Made with Cursor

Sei commits blocks faster than once a second, so the Ethereum-standard
`timestamp` — whole seconds — repeats across consecutive blocks and
cannot tell them apart. The Tendermint header already carries the full
precision; the RPC layer was discarding it at serialization.

Add a Sei-only `timestampMs` alongside `timestamp`, which stays in
seconds because every Ethereum client reads it that way. Emitted by all
four block-header encoders: EncodeTmBlock (eth_getBlockByNumber /
eth_getBlockByHash), encodeGenesisBlock, and both eth_subscribe
("newHeads") encoders.

Serialization only. The seconds-based values feeding fork rules and the
TIMESTAMP opcode are untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 27, 2026, 11:58 PM

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.27%. Comparing base (bafdcf3) to head (06610d7).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4018      +/-   ##
==========================================
- Coverage   61.22%   60.27%   -0.95%     
==========================================
  Files        2153     2059      -94     
  Lines      188393   176563   -11830     
==========================================
- Hits       115351   106432    -8919     
+ Misses      62298    60422    -1876     
+ Partials    10744     9709    -1035     
Flag Coverage Δ
sei-chain-pr 72.29% <100.00%> (?)
sei-db 70.02% <ø> (+0.21%) ⬆️
sei-db-state-db ?

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

Files with missing lines Coverage Δ
evmrpc/block.go 81.41% <100.00%> (+0.12%) ⬆️
evmrpc/subscribe.go 73.42% <100.00%> (+0.18%) ⬆️

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

@alexander-sei
alexander-sei marked this pull request as ready for review August 26, 2026 13:18

@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.

Additive, serialization-only change that exposes a Sei-only timestampMs from all four EVM block-header encoders; consensus and execution paths are correctly untouched and the geth-parity allowlists are updated. No blockers — two documentation/test-precision suggestions.

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.

Comment thread evmrpc/AGENTS.md Outdated
Comment thread evmrpc/tests/block_test.go Outdated
encodeGenesisBlock is a separate encoder, not a branch of EncodeTmBlock:
both getBlockByNumber and getBlockByHash return it before EncodeTmBlock
is reached. Naming it explicitly in evmrpc/AGENTS.md, so the next header
field added does not silently miss the genesis response.

TestGetBlockTimestampMs asserts against mockBlockHeader, whose time has
zero nanoseconds, so it cannot distinguish timestampMs from timestamp*1000.
Its nanoseconds cannot change without moving the block hashes this package
asserts against, so record what the test actually proves and where the
millisecond value is covered instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@masih

masih commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@seidroid review

@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.

Additive, serialization-only change exposing a Sei-only timestampMs from all four EVM block-header encoders; consensus/execution paths and the seconds-based timestamp are correctly untouched, and the geth-parity allowlist is updated so only Sei carries the field. Both suggestions from the previous review are resolved in the current head and I found no new issues.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.

@masih masih 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.

BEP-520 is worth a read in terms of learnings but otherwise lgtm; 🙌

We also need to make sure this is backward compatible (for indexers too and the proto edge case is a non-issue -- let's follow up on slack)

Comment thread evmrpc/AGENTS.md Outdated
- **No Trie** - Sei does not store states in a trie, so any endpoint relevant to the trie data structure is not supported.
- **No PoW** - Sei has never used proof-of-work, so endpoints like `eth_mining` and `eth_hashrate` are not supported.
- **No Blobs** - Sei does not support EIP-4844 blob transactions. `eth_blobBaseFee` returns JSON-RPC error code `-32000` with message `blobs not supported on this chain`.
- **`timestampMs`** — Sei-only block-header field carrying the header time as a hex quantity of **Unix milliseconds**, alongside the standard `timestamp`, which stays in **whole seconds** because every Ethereum client reads it that way. Sei block intervals are shorter than a second, so `timestamp` repeats across consecutive blocks. `timestampMs` is **not** unique per block either: under Autobahn a proposal spaces consecutive blocks by 1µs (`minTimestampDiff`), so blocks less than a millisecond apart tie. Unrelated to the seconds-based values feeding fork rules and the `TIMESTAMP` opcode (`vm.BlockContext.Time`, `MakeSigner`, `ethtypes.Header.Time`) — those must stay in seconds.

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.

ty for adding this Alex 🙌

(remember to rename these if we settle on renaming the field)

Comment thread evmrpc/block.go Outdated
"gasLimit": hexutil.Uint64(0),
"gasUsed": hexutil.Uint64(0),
"timestamp": hexutil.Uint64(0),
"timestampMs": hexutil.Uint64(0), // Sei-only, see EncodeTmBlock

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.

Would it make sense to pick the same name as pre-existing work (example), i.e. milliTimestamp? It reads better IMHO unless there is a clash issue of sorts for us to worry about?

Rename the block header field `timestampMs` to `milliTimestamp` (BEP-520-compatible Unix milliseconds) across the codebase and docs. Update encoders (genesis, tm block/header), subscription/notifier code paths, unit tests, WS/integration tests, and JS/TS test utilities. Preserve the existing seconds-based `timestamp` semantics; `milliTimestamp` exposes sub-second precision. Tests and assertions were adjusted to use the new field name and messaging updated to reflect BEP-520 compatibility.
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.

3 participants