From d8e7c74af4e138be2dcc6f8c0745fcf2adcab7ad Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Wed, 26 Aug 2026 13:51:58 +0200 Subject: [PATCH 1/3] Expose block time in milliseconds on the EVM RPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- evmrpc/AGENTS.md | 1 + evmrpc/block.go | 16 ++++++++---- evmrpc/block_test.go | 26 +++++++++++++++++++ evmrpc/notifier_internal_test.go | 6 ++++- evmrpc/subscribe.go | 14 +++++----- evmrpc/subscribe_test.go | 16 +++++++++--- evmrpc/tests/block_test.go | 21 +++++++++++++++ .../evm_module/ws_test/ws_test.go | 12 +++++---- .../rpc_tests/eth/eth_getBlockByHash.spec.ts | 2 ++ .../eth/eth_getBlockByNumber.spec.ts | 14 ++++++++++ .../rpc_tests/utils/subscribeUtils.ts | 5 ++++ integration_test/rpc_tests/utils/txUtils.ts | 18 ++++++++++++- 12 files changed, 130 insertions(+), 21 deletions(-) diff --git a/evmrpc/AGENTS.md b/evmrpc/AGENTS.md index 47e7e28c41..2c1b8c0567 100644 --- a/evmrpc/AGENTS.md +++ b/evmrpc/AGENTS.md @@ -30,6 +30,7 @@ EVM RPCs prefixed by `eth_` and `debug_` on Sei generally follows [Ethereum's sp - **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. Emitted by every block-header encoder — `EncodeTmBlock` (`eth_getBlockByNumber` / `eth_getBlockByHash`, including the synthetic genesis block) and both `eth_subscribe("newHeads")` encoders, `encodeCommittedBlock` (Autobahn) and `encodeTmHeader` (CometBFT) — so a header from any of them carries it. It is **not** unique per block: 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. - **Explicitly unsupported RPCs (same `-32000` pattern)** — Methods are registered so clients get a clear error instead of `-32601` method not found: - `debug_getRawBlock`, `debug_getRawHeader`, `debug_getRawReceipts`, `debug_getRawTransaction` - `eth_newPendingTransactionFilter` diff --git a/evmrpc/block.go b/evmrpc/block.go index 83bc2b91f8..1f566854e7 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -56,6 +56,7 @@ func encodeGenesisBlock() map[string]any { "gasLimit": hexutil.Uint64(0), "gasUsed": hexutil.Uint64(0), "timestamp": hexutil.Uint64(0), + "timestampMs": hexutil.Uint64(0), // Sei-only, see EncodeTmBlock "transactionsRoot": common.Hash{}, "receiptsRoot": common.Hash{}, "size": hexutil.Uint64(0), @@ -419,6 +420,10 @@ func EncodeTmBlock( if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil { gasLimit = cp.Block.MaxGas } + // "timestamp" stays in whole seconds because every Ethereum client reads it + // that way, and Sei block intervals are shorter than a second, so it repeats + // across consecutive blocks. "timestampMs" is the Sei-only companion exposing + // the sub-second precision the Tendermint header already carries. result := map[string]any{ "number": (*hexutil.Big)(number), "hash": blockhash, @@ -429,11 +434,12 @@ func EncodeTmBlock( "logsBloom": ethtypes.BytesToBloom(blockBloom), "stateRoot": appHash, "miner": miner, - "difficulty": (*hexutil.Big)(big.NewInt(0)), // inapplicable to Sei - "extraData": hexutil.Bytes{}, // inapplicable to Sei - "gasLimit": hexutil.Uint64(gasLimit), //nolint:gosec - "gasUsed": hexutil.Uint64(blockGasUsed), //nolint:gosec - "timestamp": hexutil.Uint64(block.Block.Time.Unix()), //nolint:gosec + "difficulty": (*hexutil.Big)(big.NewInt(0)), // inapplicable to Sei + "extraData": hexutil.Bytes{}, // inapplicable to Sei + "gasLimit": hexutil.Uint64(gasLimit), //nolint:gosec + "gasUsed": hexutil.Uint64(blockGasUsed), //nolint:gosec + "timestamp": hexutil.Uint64(block.Block.Time.Unix()), //nolint:gosec + "timestampMs": hexutil.Uint64(block.Block.Time.UnixMilli()), //nolint:gosec "transactionsRoot": txHash, "receiptsRoot": resultHash, "size": hexutil.Uint64(block.Block.Size()), //nolint:gosec diff --git a/evmrpc/block_test.go b/evmrpc/block_test.go index 9c939fe383..a661dd93fc 100644 --- a/evmrpc/block_test.go +++ b/evmrpc/block_test.go @@ -46,6 +46,32 @@ func TestEncodeTmBlock_EmptyTransactions(t *testing.T) { require.Equal(t, ethtypes.EmptyTxsHash, result["transactionsRoot"]) } +// Sei commits blocks more often than once a second, so the seconds-only +// Ethereum "timestamp" repeats across consecutive blocks. timestampMs carries +// the sub-second part the Tendermint header already holds. +func TestEncodeTmBlockTimestampMs(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + header := mockBlockHeader(MockHeight8) + header.Time = time.Unix(1696941649, 125_000_000).UTC() + block := &coretypes.ResultBlock{ + BlockID: MockBlockID, + Block: &tmtypes.Block{ + Header: header, + Data: tmtypes.Data{}, + LastCommit: &tmtypes.Commit{ + Height: MockHeight8 - 1, + }, + }, + } + + result, err := evmrpc.EncodeTmBlock(func(i int64) sdk.Context { return ctx }, func(i int64) client.TxConfig { return TxConfig }, block, k, true, false, evmrpc.NewBlockCache(3000), &sync.Mutex{}) + require.Nil(t, err) + + require.Equal(t, hexutil.Uint64(1696941649), result["timestamp"]) + require.Equal(t, hexutil.Uint64(1696941649125), result["timestampMs"]) +} + func TestEncodeBankMsg(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) diff --git a/evmrpc/notifier_internal_test.go b/evmrpc/notifier_internal_test.go index ef6681d6aa..e1424d5b74 100644 --- a/evmrpc/notifier_internal_test.go +++ b/evmrpc/notifier_internal_test.go @@ -144,7 +144,10 @@ func TestEncodeCommittedBlock(t *testing.T) { hash := common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111").Bytes() proposer := common.HexToAddress("0x2222222222222222222222222222222222222222").Bytes() appHash := common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333").Bytes() - ts := time.Unix(1_700_000_000, 0).UTC() + // Fractional second on purpose: it is the part "timestamp" drops and + // "timestampMs" keeps, so a whole-second fixture would pass even if + // timestampMs were derived as timestamp*1000. + ts := time.Unix(1_700_000_000, 750_000_000).UTC() evt := blockHeaderEvent{ hash: hash, header: &tmproto.Header{ @@ -168,6 +171,7 @@ func TestEncodeCommittedBlock(t *testing.T) { require.Equal(t, common.BytesToAddress(proposer), out["miner"]) require.Equal(t, common.BytesToHash(appHash), out["stateRoot"]) require.Equal(t, hexutil.Uint64(ts.Unix()), out["timestamp"]) + require.Equal(t, hexutil.Uint64(ts.UnixMilli()), out["timestampMs"]) require.Equal(t, hexutil.Uint64(121000), out["gasUsed"]) require.Equal(t, hexutil.Uint64(10_000_000), out["gasLimit"]) require.Equal(t, (*hexutil.Big)(big.NewInt(42)), out["baseFeePerGas"]) diff --git a/evmrpc/subscribe.go b/evmrpc/subscribe.go index 7506c89530..833619b29e 100644 --- a/evmrpc/subscribe.go +++ b/evmrpc/subscribe.go @@ -456,11 +456,12 @@ func encodeCommittedBlock(evt blockHeaderEvent, baseFee *big.Int, gasLimit int64 "receiptsRoot": common.Hash{}, // see function doc "sha3Uncles": common.Hash{}, // inapplicable to Sei "stateRoot": appHash, - "timestamp": hexutil.Uint64(evt.header.Time.Unix()), //nolint:gosec - "transactionsRoot": common.Hash{}, // see function doc - "mixHash": common.Hash{}, // inapplicable to Sei - "excessBlobGas": hexutil.Uint64(0), // inapplicable to Sei - "parentBeaconBlockRoot": common.Hash{}, // inapplicable to Sei + "timestamp": hexutil.Uint64(evt.header.Time.Unix()), //nolint:gosec + "timestampMs": hexutil.Uint64(evt.header.Time.UnixMilli()), //nolint:gosec + "transactionsRoot": common.Hash{}, // see function doc + "mixHash": common.Hash{}, // inapplicable to Sei + "excessBlobGas": hexutil.Uint64(0), // inapplicable to Sei + "parentBeaconBlockRoot": common.Hash{}, // inapplicable to Sei "hash": blockHash, "baseFeePerGas": (*hexutil.Big)(baseFee), "withdrawalsRoot": common.Hash{}, // inapplicable to Sei @@ -497,7 +498,8 @@ func encodeTmHeader( "receiptsRoot": resultHash, "sha3Uncles": common.Hash{}, // inapplicable to Sei "stateRoot": appHash, - "timestamp": hexutil.Uint64(header.Header.Time.Unix()), //nolint:gosec + "timestamp": hexutil.Uint64(header.Header.Time.Unix()), //nolint:gosec + "timestampMs": hexutil.Uint64(header.Header.Time.UnixMilli()), //nolint:gosec "transactionsRoot": txHash, "mixHash": common.Hash{}, // inapplicable to Sei "excessBlobGas": hexutil.Uint64(0), // inapplicable to Sei diff --git a/evmrpc/subscribe_test.go b/evmrpc/subscribe_test.go index e3a229c561..f785b8211d 100644 --- a/evmrpc/subscribe_test.go +++ b/evmrpc/subscribe_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/sei-protocol/sei-chain/evmrpc" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" @@ -25,7 +26,7 @@ func TestSubscribeNewHeads(t *testing.T) { expectedKeys := []string{ "parentHash", "sha3Uncles", "miner", "stateRoot", "transactionsRoot", "receiptsRoot", "logsBloom", "difficulty", "number", "gasLimit", - "gasUsed", "timestamp", "extraData", "mixHash", "nonce", + "gasUsed", "timestamp", "timestampMs", "extraData", "mixHash", "nonce", "baseFeePerGas", "withdrawalsRoot", "blobGasUsed", "excessBlobGas", "parentBeaconBlockRoot", "hash", } @@ -76,6 +77,13 @@ func TestSubscribeNewHeads(t *testing.T) { } } } + // This path's header time is time.Now(), so only the relationship + // between the two timestamp fields is assertable here. + secs, err := hexutil.DecodeUint64(resultMap["timestamp"].(string)) + require.NoError(t, err) + millis, err := hexutil.DecodeUint64(resultMap["timestampMs"].(string)) + require.NoError(t, err) + require.Equal(t, secs, millis/1000, "timestampMs and timestamp describe the same instant") // Event validated successfully, no need to wait further return case <-timer.C: @@ -97,12 +105,13 @@ func TestSubscribeNewHeadsAutobahn(t *testing.T) { hash := common.HexToHash("0x4242424242424242424242424242424242424242424242424242424242424242").Bytes() appHash := common.HexToHash("0x3131313131313131313131313131313131313131313131313131313131313131").Bytes() proposer := common.HexToAddress("0x9999999999999999999999999999999999999999").Bytes() - ts := time.Unix(1_700_000_500, 0).UTC() + // Fractional second on purpose; see TestEncodeCommittedBlock. + ts := time.Unix(1_700_000_500, 250_000_000).UTC() expectedKeys := []string{ "parentHash", "sha3Uncles", "miner", "stateRoot", "transactionsRoot", "receiptsRoot", "logsBloom", "difficulty", "number", "gasLimit", - "gasUsed", "timestamp", "extraData", "mixHash", "nonce", + "gasUsed", "timestamp", "timestampMs", "extraData", "mixHash", "nonce", "baseFeePerGas", "withdrawalsRoot", "blobGasUsed", "excessBlobGas", "parentBeaconBlockRoot", "hash", } @@ -149,6 +158,7 @@ func TestSubscribeNewHeadsAutobahn(t *testing.T) { require.Equal(t, common.BytesToAddress(proposer).Hex(), resultMap["miner"]) require.Equal(t, common.BytesToHash(appHash).Hex(), resultMap["stateRoot"]) require.Equal(t, fmt.Sprintf("0x%x", ts.Unix()), resultMap["timestamp"]) + require.Equal(t, fmt.Sprintf("0x%x", ts.UnixMilli()), resultMap["timestampMs"]) require.Equal(t, fmt.Sprintf("0x%x", 21000+50000), resultMap["gasUsed"]) // gasLimit comes from the SDK ConsensusParams that the test // runtime sets; just assert it's a non-zero hex string. diff --git a/evmrpc/tests/block_test.go b/evmrpc/tests/block_test.go index ed31b3b5f4..71ef925c5e 100644 --- a/evmrpc/tests/block_test.go +++ b/evmrpc/tests/block_test.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/bitutil" + "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/sei-protocol/sei-chain/app" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" @@ -52,6 +53,26 @@ func TestGetBlockByNumber(t *testing.T) { ) } +// Covers the Sei-only timestampMs over the real JSON-RPC transport, including the +// synthetic genesis block, which is encoded by a different function than the rest. +func TestGetBlockTimestampMs(t *testing.T) { + txBz := signAndEncodeTx(send(0), mnemonic1) + SetupTestServer(t, [][][]byte{{txBz}}, mnemonicInitializer(mnemonic1)).Run( + func(port int) { + res := sendRequestWithNamespace("eth", port, "getBlockByNumber", "0x2", false) + block := res["result"].(map[string]interface{}) + blockTime := mockBlockHeader(2).Time + require.Equal(t, hexutil.EncodeUint64(uint64(blockTime.Unix())), block["timestamp"]) + require.Equal(t, hexutil.EncodeUint64(uint64(blockTime.UnixMilli())), block["timestampMs"]) + + res = sendRequestWithNamespace("eth", port, "getBlockByNumber", "earliest", false) + genesis := res["result"].(map[string]interface{}) + require.Equal(t, "0x0", genesis["timestamp"]) + require.Equal(t, "0x0", genesis["timestampMs"]) + }, + ) +} + func TestGetBlockSkipTxIndex(t *testing.T) { tx1 := signAndEncodeCosmosTx(bankSendMsg(mnemonic1), mnemonic1, 7, 0) tx2 := signAndEncodeTx(send(0), mnemonic1) diff --git a/integration_test/evm_module/ws_test/ws_test.go b/integration_test/evm_module/ws_test/ws_test.go index 2db984a66f..5e669b4dd4 100644 --- a/integration_test/evm_module/ws_test/ws_test.go +++ b/integration_test/evm_module/ws_test/ws_test.go @@ -107,16 +107,18 @@ func TestEthSubscribeNewHeads(t *testing.T) { note.Params.Subscription, ack.Result) } header := note.Params.Result - for _, key := range []string{"hash", "number", "timestamp", "stateRoot", "miner"} { + // An all-zero value for one of these would indicate the producer hook didn't + // fire and we're seeing a default-constructed header. + mustBeNonZero := map[string]bool{ + "hash": true, "number": true, "timestamp": true, "timestampMs": true, + } + for _, key := range []string{"hash", "number", "timestamp", "timestampMs", "stateRoot", "miner"} { v, ok := header[key] if !ok { t.Fatalf("head notification missing key %q (got %+v)", key, header) } - // All-zero values for hash, number, or timestamp would indicate - // the producer hook didn't fire and we're seeing a default- - // constructed header. if s, _ := v.(string); s == "" || s == "0x" || s == "0x0" { - if key == "hash" || key == "number" || key == "timestamp" { + if mustBeNonZero[key] { t.Fatalf("head notification %q has zero-ish value %q", key, s) } } diff --git a/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts b/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts index b3d6aaa311..4a2534828d 100644 --- a/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts +++ b/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts @@ -10,6 +10,7 @@ import { sendRevertingTx, signBelowIntrinsicTx, assertCanonicalHeader, + assertSeiTimestampMs, assertCanonicalTx, assertGasAccounting, assertActualBytesAndSize, @@ -72,6 +73,7 @@ describe('eth_getBlockByHash', function () { it('returns every canonical header field and echoes the requested hash', async () => { const block = await byHash(sei, richSei.hash, false); assertCanonicalHeader(block, { hasTxs: true }); + assertSeiTimestampMs(block); expect(block.hash).to.equal(richSei.hash); expect(BigInt(block.number)).to.equal(BigInt(richSei.number)); }); diff --git a/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts b/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts index dfce91b805..beecb70ee2 100644 --- a/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts +++ b/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts @@ -10,6 +10,7 @@ import { sendRevertingTx, signBelowIntrinsicTx, assertCanonicalHeader, + assertSeiTimestampMs, assertCanonicalTx, assertTxTypeSchema, assertGasAccounting, @@ -82,6 +83,19 @@ describe('eth_getBlockByNumber', function () { expect(block.parentHash).to.equal(parent.hash); expect(BigInt(block.timestamp) >= BigInt(parent.timestamp)).to.equal(true); }); + + it('carries the Sei-only millisecond timestamp', async () => { + const [block, parent] = await Promise.all([ + getBlock(sei, richSei.number, false), + getBlock(sei, richSei.number - 1, false), + ]); + assertSeiTimestampMs(block); + assertSeiTimestampMs(parent); + expect( + BigInt(block.timestampMs!) >= BigInt(parent.timestampMs!), + 'timestampMs is non-decreasing along the chain', + ).to.equal(true); + }); }); describe('transactions array (hashes vs full objects)', () => { diff --git a/integration_test/rpc_tests/utils/subscribeUtils.ts b/integration_test/rpc_tests/utils/subscribeUtils.ts index 208334e9aa..36c55ee419 100644 --- a/integration_test/rpc_tests/utils/subscribeUtils.ts +++ b/integration_test/rpc_tests/utils/subscribeUtils.ts @@ -109,6 +109,7 @@ export const NEW_HEAD_FIELDS = [ 'sha3Uncles', 'stateRoot', 'timestamp', + 'timestampMs', 'transactionsRoot', 'withdrawalsRoot', ] as const; @@ -118,6 +119,7 @@ const HEAD_QUANTITY_FIELDS = [ 'gasLimit', 'gasUsed', 'timestamp', + 'timestampMs', 'difficulty', 'baseFeePerGas', 'blobGasUsed', @@ -172,6 +174,9 @@ export function assertNewHeadMatchesBlock(head: any, block: any): void { expect(BigInt(head.timestamp), 'timestamp matches the canonical block').to.equal( BigInt(block.timestamp), ); + expect(BigInt(head.timestampMs), 'timestampMs matches the canonical block').to.equal( + BigInt(block.timestampMs), + ); expect(BigInt(head.gasLimit), 'gasLimit matches the canonical block').to.equal( BigInt(block.gasLimit), ); diff --git a/integration_test/rpc_tests/utils/txUtils.ts b/integration_test/rpc_tests/utils/txUtils.ts index addc0bcb83..7fad4f3715 100644 --- a/integration_test/rpc_tests/utils/txUtils.ts +++ b/integration_test/rpc_tests/utils/txUtils.ts @@ -44,7 +44,7 @@ export const CORE_BLOCK_FIELDS = [ 'uncles', ] as const; -export const SEI_ONLY_BLOCK_FIELDS = ['totalDifficulty'] as const; +export const SEI_ONLY_BLOCK_FIELDS = ['timestampMs', 'totalDifficulty'] as const; export const GETH_ONLY_BLOCK_FIELDS = [ 'blobGasUsed', 'excessBlobGas', @@ -167,6 +167,8 @@ export interface RpcBlock { gasLimit: string; gasUsed: string; timestamp: string; + /** Sei-only; geth blocks parsed into this shape do not carry it. */ + timestampMs?: string; baseFeePerGas: string; uncles: string[]; transactions: (string | RpcTx)[]; @@ -727,6 +729,20 @@ export function assertCanonicalHeader(block: RpcBlock, opts: { hasTxs: boolean } } } +/** + * Assert the Sei-only `timestampMs`. Sei commits blocks faster than once a second, so + * `timestamp` — whole seconds, as every Ethereum client reads it — cannot separate + * consecutive blocks. geth has no counterpart, so there is nothing to cross-check against; + * the invariant is that the two fields describe the same instant. + */ +export function assertSeiTimestampMs(block: RpcBlock): void { + expect(block, 'header is missing timestampMs').to.have.property('timestampMs'); + expect(block.timestampMs, 'timestampMs is a canonical quantity').to.match(HEX_QUANTITY); + expect(BigInt(block.timestampMs!) / 1000n, 'timestampMs agrees with timestamp').to.equal( + BigInt(block.timestamp), + ); +} + // Fields present on every transaction object regardless of type (legacy type-0 has // no accessList / maxFeePerGas, so those live in the type-2 CORE_TX_FIELDS set used // only by the geth parity comparison). From 482ef20857396cc9b61663066befd3c839bdedf5 Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Wed, 26 Aug 2026 15:31:59 +0200 Subject: [PATCH 2/3] Address review: name all four header encoders, scope the HTTP test claim 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 --- evmrpc/AGENTS.md | 7 ++++++- evmrpc/tests/block_test.go | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/evmrpc/AGENTS.md b/evmrpc/AGENTS.md index 2c1b8c0567..cb14aae298 100644 --- a/evmrpc/AGENTS.md +++ b/evmrpc/AGENTS.md @@ -30,7 +30,12 @@ EVM RPCs prefixed by `eth_` and `debug_` on Sei generally follows [Ethereum's sp - **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. Emitted by every block-header encoder — `EncodeTmBlock` (`eth_getBlockByNumber` / `eth_getBlockByHash`, including the synthetic genesis block) and both `eth_subscribe("newHeads")` encoders, `encodeCommittedBlock` (Autobahn) and `encodeTmHeader` (CometBFT) — so a header from any of them carries it. It is **not** unique per block: 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. +- **`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. +- **Four separate encoders build block headers.** A field added to one is silently absent from the other three, so a new header field has to be added to all four: + - `EncodeTmBlock` — `eth_getBlockByNumber` / `eth_getBlockByHash` + - `encodeGenesisBlock` — the synthetic genesis block; both of the above return it early, before `EncodeTmBlock` is reached + - `encodeCommittedBlock` — `eth_subscribe("newHeads")` under Autobahn + - `encodeTmHeader` — `eth_subscribe("newHeads")` under CometBFT - **Explicitly unsupported RPCs (same `-32000` pattern)** — Methods are registered so clients get a clear error instead of `-32601` method not found: - `debug_getRawBlock`, `debug_getRawHeader`, `debug_getRawReceipts`, `debug_getRawTransaction` - `eth_newPendingTransactionFilter` diff --git a/evmrpc/tests/block_test.go b/evmrpc/tests/block_test.go index 71ef925c5e..009f1ac0db 100644 --- a/evmrpc/tests/block_test.go +++ b/evmrpc/tests/block_test.go @@ -55,6 +55,13 @@ func TestGetBlockByNumber(t *testing.T) { // Covers the Sei-only timestampMs over the real JSON-RPC transport, including the // synthetic genesis block, which is encoded by a different function than the rest. +// +// This pins encoding and presence at second granularity only: mockBlockHeader builds +// its time with time.Unix(_, 0), so UnixMilli() here is just Unix()*1000, and its +// nanoseconds cannot be changed without moving the block hashes the rest of this +// package asserts against. The millisecond value is proven where a fractional-second +// header is available: TestEncodeTmBlockTimestampMs for the encoder, and +// TestSubscribeNewHeadsAutobahn over the WS transport. func TestGetBlockTimestampMs(t *testing.T) { txBz := signAndEncodeTx(send(0), mnemonic1) SetupTestServer(t, [][][]byte{{txBz}}, mnemonicInitializer(mnemonic1)).Run( From 06610d735b85833c5fa1d6f14dc5d98ab6836955 Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Fri, 28 Aug 2026 01:57:22 +0200 Subject: [PATCH 3/3] Replace timestampMs with milliTimestamp 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. --- evmrpc/AGENTS.md | 2 +- evmrpc/block.go | 8 ++++---- evmrpc/block_test.go | 6 +++--- evmrpc/notifier_internal_test.go | 6 +++--- evmrpc/subscribe.go | 4 ++-- evmrpc/subscribe_test.go | 10 +++++----- evmrpc/tests/block_test.go | 10 +++++----- integration_test/evm_module/ws_test/ws_test.go | 4 ++-- .../rpc_tests/eth/eth_getBlockByHash.spec.ts | 4 ++-- .../rpc_tests/eth/eth_getBlockByNumber.spec.ts | 12 ++++++------ .../rpc_tests/utils/subscribeUtils.ts | 8 ++++---- integration_test/rpc_tests/utils/txUtils.ts | 16 ++++++++-------- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/evmrpc/AGENTS.md b/evmrpc/AGENTS.md index cb14aae298..aab3df8e2d 100644 --- a/evmrpc/AGENTS.md +++ b/evmrpc/AGENTS.md @@ -30,7 +30,7 @@ EVM RPCs prefixed by `eth_` and `debug_` on Sei generally follows [Ethereum's sp - **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. +- **`milliTimestamp`** — BEP-520-compatible 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. `milliTimestamp` 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. - **Four separate encoders build block headers.** A field added to one is silently absent from the other three, so a new header field has to be added to all four: - `EncodeTmBlock` — `eth_getBlockByNumber` / `eth_getBlockByHash` - `encodeGenesisBlock` — the synthetic genesis block; both of the above return it early, before `EncodeTmBlock` is reached diff --git a/evmrpc/block.go b/evmrpc/block.go index 1f566854e7..bab26c9a56 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -56,7 +56,7 @@ func encodeGenesisBlock() map[string]any { "gasLimit": hexutil.Uint64(0), "gasUsed": hexutil.Uint64(0), "timestamp": hexutil.Uint64(0), - "timestampMs": hexutil.Uint64(0), // Sei-only, see EncodeTmBlock + "milliTimestamp": hexutil.Uint64(0), // BEP-520-compatible, see EncodeTmBlock "transactionsRoot": common.Hash{}, "receiptsRoot": common.Hash{}, "size": hexutil.Uint64(0), @@ -422,8 +422,8 @@ func EncodeTmBlock( } // "timestamp" stays in whole seconds because every Ethereum client reads it // that way, and Sei block intervals are shorter than a second, so it repeats - // across consecutive blocks. "timestampMs" is the Sei-only companion exposing - // the sub-second precision the Tendermint header already carries. + // across consecutive blocks. "milliTimestamp" is the BEP-520-compatible + // companion exposing the sub-second precision the Tendermint header already carries. result := map[string]any{ "number": (*hexutil.Big)(number), "hash": blockhash, @@ -439,7 +439,7 @@ func EncodeTmBlock( "gasLimit": hexutil.Uint64(gasLimit), //nolint:gosec "gasUsed": hexutil.Uint64(blockGasUsed), //nolint:gosec "timestamp": hexutil.Uint64(block.Block.Time.Unix()), //nolint:gosec - "timestampMs": hexutil.Uint64(block.Block.Time.UnixMilli()), //nolint:gosec + "milliTimestamp": hexutil.Uint64(block.Block.Time.UnixMilli()), //nolint:gosec "transactionsRoot": txHash, "receiptsRoot": resultHash, "size": hexutil.Uint64(block.Block.Size()), //nolint:gosec diff --git a/evmrpc/block_test.go b/evmrpc/block_test.go index a661dd93fc..097c7221f4 100644 --- a/evmrpc/block_test.go +++ b/evmrpc/block_test.go @@ -47,9 +47,9 @@ func TestEncodeTmBlock_EmptyTransactions(t *testing.T) { } // Sei commits blocks more often than once a second, so the seconds-only -// Ethereum "timestamp" repeats across consecutive blocks. timestampMs carries +// Ethereum "timestamp" repeats across consecutive blocks. milliTimestamp carries // the sub-second part the Tendermint header already holds. -func TestEncodeTmBlockTimestampMs(t *testing.T) { +func TestEncodeTmBlockMilliTimestamp(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) header := mockBlockHeader(MockHeight8) @@ -69,7 +69,7 @@ func TestEncodeTmBlockTimestampMs(t *testing.T) { require.Nil(t, err) require.Equal(t, hexutil.Uint64(1696941649), result["timestamp"]) - require.Equal(t, hexutil.Uint64(1696941649125), result["timestampMs"]) + require.Equal(t, hexutil.Uint64(1696941649125), result["milliTimestamp"]) } func TestEncodeBankMsg(t *testing.T) { diff --git a/evmrpc/notifier_internal_test.go b/evmrpc/notifier_internal_test.go index e1424d5b74..66bdb5e97f 100644 --- a/evmrpc/notifier_internal_test.go +++ b/evmrpc/notifier_internal_test.go @@ -145,8 +145,8 @@ func TestEncodeCommittedBlock(t *testing.T) { proposer := common.HexToAddress("0x2222222222222222222222222222222222222222").Bytes() appHash := common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333").Bytes() // Fractional second on purpose: it is the part "timestamp" drops and - // "timestampMs" keeps, so a whole-second fixture would pass even if - // timestampMs were derived as timestamp*1000. + // "milliTimestamp" keeps, so a whole-second fixture would pass even if + // milliTimestamp were derived as timestamp*1000. ts := time.Unix(1_700_000_000, 750_000_000).UTC() evt := blockHeaderEvent{ hash: hash, @@ -171,7 +171,7 @@ func TestEncodeCommittedBlock(t *testing.T) { require.Equal(t, common.BytesToAddress(proposer), out["miner"]) require.Equal(t, common.BytesToHash(appHash), out["stateRoot"]) require.Equal(t, hexutil.Uint64(ts.Unix()), out["timestamp"]) - require.Equal(t, hexutil.Uint64(ts.UnixMilli()), out["timestampMs"]) + require.Equal(t, hexutil.Uint64(ts.UnixMilli()), out["milliTimestamp"]) require.Equal(t, hexutil.Uint64(121000), out["gasUsed"]) require.Equal(t, hexutil.Uint64(10_000_000), out["gasLimit"]) require.Equal(t, (*hexutil.Big)(big.NewInt(42)), out["baseFeePerGas"]) diff --git a/evmrpc/subscribe.go b/evmrpc/subscribe.go index 833619b29e..70f8ff2956 100644 --- a/evmrpc/subscribe.go +++ b/evmrpc/subscribe.go @@ -457,7 +457,7 @@ func encodeCommittedBlock(evt blockHeaderEvent, baseFee *big.Int, gasLimit int64 "sha3Uncles": common.Hash{}, // inapplicable to Sei "stateRoot": appHash, "timestamp": hexutil.Uint64(evt.header.Time.Unix()), //nolint:gosec - "timestampMs": hexutil.Uint64(evt.header.Time.UnixMilli()), //nolint:gosec + "milliTimestamp": hexutil.Uint64(evt.header.Time.UnixMilli()), //nolint:gosec "transactionsRoot": common.Hash{}, // see function doc "mixHash": common.Hash{}, // inapplicable to Sei "excessBlobGas": hexutil.Uint64(0), // inapplicable to Sei @@ -499,7 +499,7 @@ func encodeTmHeader( "sha3Uncles": common.Hash{}, // inapplicable to Sei "stateRoot": appHash, "timestamp": hexutil.Uint64(header.Header.Time.Unix()), //nolint:gosec - "timestampMs": hexutil.Uint64(header.Header.Time.UnixMilli()), //nolint:gosec + "milliTimestamp": hexutil.Uint64(header.Header.Time.UnixMilli()), //nolint:gosec "transactionsRoot": txHash, "mixHash": common.Hash{}, // inapplicable to Sei "excessBlobGas": hexutil.Uint64(0), // inapplicable to Sei diff --git a/evmrpc/subscribe_test.go b/evmrpc/subscribe_test.go index f785b8211d..3af01bd161 100644 --- a/evmrpc/subscribe_test.go +++ b/evmrpc/subscribe_test.go @@ -26,7 +26,7 @@ func TestSubscribeNewHeads(t *testing.T) { expectedKeys := []string{ "parentHash", "sha3Uncles", "miner", "stateRoot", "transactionsRoot", "receiptsRoot", "logsBloom", "difficulty", "number", "gasLimit", - "gasUsed", "timestamp", "timestampMs", "extraData", "mixHash", "nonce", + "gasUsed", "timestamp", "milliTimestamp", "extraData", "mixHash", "nonce", "baseFeePerGas", "withdrawalsRoot", "blobGasUsed", "excessBlobGas", "parentBeaconBlockRoot", "hash", } @@ -81,9 +81,9 @@ func TestSubscribeNewHeads(t *testing.T) { // between the two timestamp fields is assertable here. secs, err := hexutil.DecodeUint64(resultMap["timestamp"].(string)) require.NoError(t, err) - millis, err := hexutil.DecodeUint64(resultMap["timestampMs"].(string)) + millis, err := hexutil.DecodeUint64(resultMap["milliTimestamp"].(string)) require.NoError(t, err) - require.Equal(t, secs, millis/1000, "timestampMs and timestamp describe the same instant") + require.Equal(t, secs, millis/1000, "milliTimestamp and timestamp describe the same instant") // Event validated successfully, no need to wait further return case <-timer.C: @@ -111,7 +111,7 @@ func TestSubscribeNewHeadsAutobahn(t *testing.T) { expectedKeys := []string{ "parentHash", "sha3Uncles", "miner", "stateRoot", "transactionsRoot", "receiptsRoot", "logsBloom", "difficulty", "number", "gasLimit", - "gasUsed", "timestamp", "timestampMs", "extraData", "mixHash", "nonce", + "gasUsed", "timestamp", "milliTimestamp", "extraData", "mixHash", "nonce", "baseFeePerGas", "withdrawalsRoot", "blobGasUsed", "excessBlobGas", "parentBeaconBlockRoot", "hash", } @@ -158,7 +158,7 @@ func TestSubscribeNewHeadsAutobahn(t *testing.T) { require.Equal(t, common.BytesToAddress(proposer).Hex(), resultMap["miner"]) require.Equal(t, common.BytesToHash(appHash).Hex(), resultMap["stateRoot"]) require.Equal(t, fmt.Sprintf("0x%x", ts.Unix()), resultMap["timestamp"]) - require.Equal(t, fmt.Sprintf("0x%x", ts.UnixMilli()), resultMap["timestampMs"]) + require.Equal(t, fmt.Sprintf("0x%x", ts.UnixMilli()), resultMap["milliTimestamp"]) require.Equal(t, fmt.Sprintf("0x%x", 21000+50000), resultMap["gasUsed"]) // gasLimit comes from the SDK ConsensusParams that the test // runtime sets; just assert it's a non-zero hex string. diff --git a/evmrpc/tests/block_test.go b/evmrpc/tests/block_test.go index 009f1ac0db..c93fedf352 100644 --- a/evmrpc/tests/block_test.go +++ b/evmrpc/tests/block_test.go @@ -53,16 +53,16 @@ func TestGetBlockByNumber(t *testing.T) { ) } -// Covers the Sei-only timestampMs over the real JSON-RPC transport, including the +// Covers the BEP-520-compatible milliTimestamp over the real JSON-RPC transport, including the // synthetic genesis block, which is encoded by a different function than the rest. // // This pins encoding and presence at second granularity only: mockBlockHeader builds // its time with time.Unix(_, 0), so UnixMilli() here is just Unix()*1000, and its // nanoseconds cannot be changed without moving the block hashes the rest of this // package asserts against. The millisecond value is proven where a fractional-second -// header is available: TestEncodeTmBlockTimestampMs for the encoder, and +// header is available: TestEncodeTmBlockMilliTimestamp for the encoder, and // TestSubscribeNewHeadsAutobahn over the WS transport. -func TestGetBlockTimestampMs(t *testing.T) { +func TestGetBlockMilliTimestamp(t *testing.T) { txBz := signAndEncodeTx(send(0), mnemonic1) SetupTestServer(t, [][][]byte{{txBz}}, mnemonicInitializer(mnemonic1)).Run( func(port int) { @@ -70,12 +70,12 @@ func TestGetBlockTimestampMs(t *testing.T) { block := res["result"].(map[string]interface{}) blockTime := mockBlockHeader(2).Time require.Equal(t, hexutil.EncodeUint64(uint64(blockTime.Unix())), block["timestamp"]) - require.Equal(t, hexutil.EncodeUint64(uint64(blockTime.UnixMilli())), block["timestampMs"]) + require.Equal(t, hexutil.EncodeUint64(uint64(blockTime.UnixMilli())), block["milliTimestamp"]) res = sendRequestWithNamespace("eth", port, "getBlockByNumber", "earliest", false) genesis := res["result"].(map[string]interface{}) require.Equal(t, "0x0", genesis["timestamp"]) - require.Equal(t, "0x0", genesis["timestampMs"]) + require.Equal(t, "0x0", genesis["milliTimestamp"]) }, ) } diff --git a/integration_test/evm_module/ws_test/ws_test.go b/integration_test/evm_module/ws_test/ws_test.go index 5e669b4dd4..beb017fad6 100644 --- a/integration_test/evm_module/ws_test/ws_test.go +++ b/integration_test/evm_module/ws_test/ws_test.go @@ -110,9 +110,9 @@ func TestEthSubscribeNewHeads(t *testing.T) { // An all-zero value for one of these would indicate the producer hook didn't // fire and we're seeing a default-constructed header. mustBeNonZero := map[string]bool{ - "hash": true, "number": true, "timestamp": true, "timestampMs": true, + "hash": true, "number": true, "timestamp": true, "milliTimestamp": true, } - for _, key := range []string{"hash", "number", "timestamp", "timestampMs", "stateRoot", "miner"} { + for _, key := range []string{"hash", "number", "timestamp", "milliTimestamp", "stateRoot", "miner"} { v, ok := header[key] if !ok { t.Fatalf("head notification missing key %q (got %+v)", key, header) diff --git a/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts b/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts index 4a2534828d..6c2b7d2fa9 100644 --- a/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts +++ b/integration_test/rpc_tests/eth/eth_getBlockByHash.spec.ts @@ -10,7 +10,7 @@ import { sendRevertingTx, signBelowIntrinsicTx, assertCanonicalHeader, - assertSeiTimestampMs, + assertSeiMilliTimestamp, assertCanonicalTx, assertGasAccounting, assertActualBytesAndSize, @@ -73,7 +73,7 @@ describe('eth_getBlockByHash', function () { it('returns every canonical header field and echoes the requested hash', async () => { const block = await byHash(sei, richSei.hash, false); assertCanonicalHeader(block, { hasTxs: true }); - assertSeiTimestampMs(block); + assertSeiMilliTimestamp(block); expect(block.hash).to.equal(richSei.hash); expect(BigInt(block.number)).to.equal(BigInt(richSei.number)); }); diff --git a/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts b/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts index beecb70ee2..73d775ee9d 100644 --- a/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts +++ b/integration_test/rpc_tests/eth/eth_getBlockByNumber.spec.ts @@ -10,7 +10,7 @@ import { sendRevertingTx, signBelowIntrinsicTx, assertCanonicalHeader, - assertSeiTimestampMs, + assertSeiMilliTimestamp, assertCanonicalTx, assertTxTypeSchema, assertGasAccounting, @@ -84,16 +84,16 @@ describe('eth_getBlockByNumber', function () { expect(BigInt(block.timestamp) >= BigInt(parent.timestamp)).to.equal(true); }); - it('carries the Sei-only millisecond timestamp', async () => { + it('carries the BEP-520-compatible millisecond timestamp', async () => { const [block, parent] = await Promise.all([ getBlock(sei, richSei.number, false), getBlock(sei, richSei.number - 1, false), ]); - assertSeiTimestampMs(block); - assertSeiTimestampMs(parent); + assertSeiMilliTimestamp(block); + assertSeiMilliTimestamp(parent); expect( - BigInt(block.timestampMs!) >= BigInt(parent.timestampMs!), - 'timestampMs is non-decreasing along the chain', + BigInt(block.milliTimestamp!) >= BigInt(parent.milliTimestamp!), + 'milliTimestamp is non-decreasing along the chain', ).to.equal(true); }); }); diff --git a/integration_test/rpc_tests/utils/subscribeUtils.ts b/integration_test/rpc_tests/utils/subscribeUtils.ts index 36c55ee419..7667eacc30 100644 --- a/integration_test/rpc_tests/utils/subscribeUtils.ts +++ b/integration_test/rpc_tests/utils/subscribeUtils.ts @@ -109,7 +109,7 @@ export const NEW_HEAD_FIELDS = [ 'sha3Uncles', 'stateRoot', 'timestamp', - 'timestampMs', + 'milliTimestamp', 'transactionsRoot', 'withdrawalsRoot', ] as const; @@ -119,7 +119,7 @@ const HEAD_QUANTITY_FIELDS = [ 'gasLimit', 'gasUsed', 'timestamp', - 'timestampMs', + 'milliTimestamp', 'difficulty', 'baseFeePerGas', 'blobGasUsed', @@ -174,8 +174,8 @@ export function assertNewHeadMatchesBlock(head: any, block: any): void { expect(BigInt(head.timestamp), 'timestamp matches the canonical block').to.equal( BigInt(block.timestamp), ); - expect(BigInt(head.timestampMs), 'timestampMs matches the canonical block').to.equal( - BigInt(block.timestampMs), + expect(BigInt(head.milliTimestamp), 'milliTimestamp matches the canonical block').to.equal( + BigInt(block.milliTimestamp), ); expect(BigInt(head.gasLimit), 'gasLimit matches the canonical block').to.equal( BigInt(block.gasLimit), diff --git a/integration_test/rpc_tests/utils/txUtils.ts b/integration_test/rpc_tests/utils/txUtils.ts index 7fad4f3715..6a45d8ef9b 100644 --- a/integration_test/rpc_tests/utils/txUtils.ts +++ b/integration_test/rpc_tests/utils/txUtils.ts @@ -44,7 +44,7 @@ export const CORE_BLOCK_FIELDS = [ 'uncles', ] as const; -export const SEI_ONLY_BLOCK_FIELDS = ['timestampMs', 'totalDifficulty'] as const; +export const SEI_ONLY_BLOCK_FIELDS = ['milliTimestamp', 'totalDifficulty'] as const; export const GETH_ONLY_BLOCK_FIELDS = [ 'blobGasUsed', 'excessBlobGas', @@ -167,8 +167,8 @@ export interface RpcBlock { gasLimit: string; gasUsed: string; timestamp: string; - /** Sei-only; geth blocks parsed into this shape do not carry it. */ - timestampMs?: string; + /** BEP-520-compatible; geth blocks parsed into this shape do not carry it. */ + milliTimestamp?: string; baseFeePerGas: string; uncles: string[]; transactions: (string | RpcTx)[]; @@ -730,15 +730,15 @@ export function assertCanonicalHeader(block: RpcBlock, opts: { hasTxs: boolean } } /** - * Assert the Sei-only `timestampMs`. Sei commits blocks faster than once a second, so + * Assert the BEP-520-compatible `milliTimestamp`. Sei commits blocks faster than once a second, so * `timestamp` — whole seconds, as every Ethereum client reads it — cannot separate * consecutive blocks. geth has no counterpart, so there is nothing to cross-check against; * the invariant is that the two fields describe the same instant. */ -export function assertSeiTimestampMs(block: RpcBlock): void { - expect(block, 'header is missing timestampMs').to.have.property('timestampMs'); - expect(block.timestampMs, 'timestampMs is a canonical quantity').to.match(HEX_QUANTITY); - expect(BigInt(block.timestampMs!) / 1000n, 'timestampMs agrees with timestamp').to.equal( +export function assertSeiMilliTimestamp(block: RpcBlock): void { + expect(block, 'header is missing milliTimestamp').to.have.property('milliTimestamp'); + expect(block.milliTimestamp, 'milliTimestamp is a canonical quantity').to.match(HEX_QUANTITY); + expect(BigInt(block.milliTimestamp!) / 1000n, 'milliTimestamp agrees with timestamp').to.equal( BigInt(block.timestamp), ); }