Expose block time in milliseconds on the EVM RPC - #4018
Conversation
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>
|
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 #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
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.
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.
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>
|
@seidroid review |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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)
| - **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. |
There was a problem hiding this comment.
ty for adding this Alex 🙌
(remember to rename these if we settle on renaming the field)
| "gasLimit": hexutil.Uint64(0), | ||
| "gasUsed": hexutil.Uint64(0), | ||
| "timestamp": hexutil.Uint64(0), | ||
| "timestampMs": hexutil.Uint64(0), // Sei-only, see EncodeTmBlock |
There was a problem hiding this comment.
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.
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) alongsidetimestamp, 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:EncodeTmBlock—eth_getBlockByNumber/eth_getBlockByHashencodeGenesisBlock— the synthetic genesis blockencodeCommittedBlock—eth_subscribe("newHeads"), AutobahnencodeTmHeader—eth_subscribe("newHeads"), CometBFTSerialization only. No consensus, storage, or execution change. The seconds-based values feeding fork rules and the
TIMESTAMPopcode (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
timestampis unchanged for every height, which keeps theevmrpc/AGENTS.mdconsistency rule (responses for historical heights must not change as the chain progresses).timestampMsis not unique per block. Under Autobahn a proposal spaces consecutive blocks by 1µs (minTimestampDiffinsei-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 inevmrpc/AGENTS.md, and the chain assertion is written as non-decreasing rather than strictly increasing.SEI_ONLY_BLOCK_FIELDSupdated — they assert that any Sei block key absent from geth is on that allowlist.timestampMswas deliberately not added toCORE_BLOCK_FIELDSorassertCanonicalHeader, since both also run against geth blocks; Sei-only coverage goes through a newassertSeiTimestampMshelper.timestamp * 1000, so they would not have been testing anything.Test plan
go test ./evmrpc/...— passesmake fmtcheck— cleango vet ./evmrpc/... ./integration_test/evm_module/ws_test/— cleanTestEncodeTmBlockTimestampMs— sub-second header time survives intotimestampMswhiletimestamptruncatesTestEncodeCommittedBlock— exact value on the Autobahn newHeads encoderTestSubscribeNewHeads— the two fields agree on the CometBFT newHeads pathTestSubscribeNewHeadsAutobahn— exact value over the real WS JSON transportTestGetBlockTimestampMs(evmrpc/tests) — over the real HTTP JSON-RPC transport, including genesisintegration_test/rpc_tests(eth_getBlockByNumber,eth_getBlockByHash,eth_subscribe) andintegration_test/evm_module/ws_testintegration_test/rpc_tests/node_modulesis not installed, so those edits are reviewed by hand rather than compiler-verifiedMade with Cursor