Skip to content
BaseHub by wbnns Updated

Denim 200ms Native Blocks

Denim is a proposed change to how Base produces blocks. Instead of one canonical block every two seconds, the chain would seal five per second. Every 200ms interval would produce a block that is complete in its own right — separately numbered and hashed, with its own state root and receipts, its own forkchoice update, and its own path from unsafe to safe to finalized.

Today Flashblocks publish incremental previews of pending state inside a single two-second block. Denim removes the need for that layer by making the 200ms interval a real block. Under the rollout Base stops emitting Flashblocks entirely, so anything reading a Flashblocks stream has to move over to canonical block and RPC feeds.

The Ethereum block header does not change. Its timestamp field stays Unix time in whole seconds, and a metadata deposit called BaseTime supplies the missing sub-second piece. Reading both together gives a block’s millisecond timestamp.

NetworkActivation timestampRequired client versions
sepoliaTBDTBD
mainnetTBDTBD

Nothing is scheduled. Upstream plans a separate node upgrade guide covering releases and rollout once there is a date.

Because the header keeps whole seconds, a block’s real time comes from combining the header with the BaseTime deposit that sits at tx[1]:

T_ms(b) = 1000 * b.header.timestamp + b.tx[1].timestamp_millis_part

Only five values of timestamp_millis_part are legal: 0, 200, 400, 600, and 800. Any two consecutive activated blocks must be exactly 200ms apart:

T_ms(child) = T_ms(parent) + 200

Slots cannot be skipped, and the two halves of the timestamp must come from one scheduled slot rather than being assembled independently. When the sequencer actually begins building has no bearing on the timestamp the block carries — the schedule decides that, not the wall clock. Contracts see no difference here: EVM block.timestamp still reports the whole-second header value.

Every block after activation carries one BaseTime update at tx[1]. It follows the L1 information deposit at tx[0] and precedes all user transactions. Source-hash domain 3 binds the deposit to the block number it belongs to.

FieldValue
Transaction typeDeposit (0x7e)
Source hashDomain 3, bound to the current block number
From0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001
To0x4200000000000000000000000000000000000030
Mint0
Value0
Gas limit1,000,000
System transactionfalse
CalldatasetTimestampMillisPart(uint16) with selector 0x86bdf394 and a 32-byte ABI-encoded millisecond part

The rule runs both ways. A block built before activation must carry neither this deposit nor the Engine millisecond field. Once activated, implementations check the transaction end to end: its position in the block, the value it writes, the shape of its calldata, its system flag and gas limit, the mint and value it declares, its recipient and sender, and its source hash.

Contracts that need the current millisecond part read it from a predeploy.

PropertyValue
Proxy0x4200000000000000000000000000000000000030
Implementation0xc0D3C0d3C0d3C0D3c0d3C0d3c0D3C0d3c0d30030
Storageuint16 millisecond part in slot 0
SettersetTimestampMillisPart(uint16)
Millisecond-part gettertimestampMillisPart()
Full-timestamp gettertimestampMs()

Ordering is what makes the value trustworthy inside a block. BaseTime executes ahead of L1 user deposits and ahead of user transactions, so everything that runs later already sees the current value.

A chain starting fresh gets the linked predeploy in genesis. On a chain that already exists, the reserved address holds the canonical proxy runtime under the Base ProxyAdmin, but with no implementation set — calls to it revert until Denim activates. Activation installs the canonical implementation and links the proxy before any transaction executes, and it leaves both the proxy admin and any implementation that governance already set in place.

The Engine API carries BasePayloadAttributes, which lays the standard PayloadAttributes fields out alongside the Base-specific ones.

At or after activation, BasePayloadAttributes.timestampMillisPart MUST be present and hold one of 0, 200, 400, 600, or 800. Before activation it MUST NOT appear at all.

BasePayloadAttributes.transactions[1] MUST hold the BaseTime deposit. The protocol depositor MUST be its sender and the BaseTime predeploy its recipient, and the calldata MUST be a canonical encoding of setTimestampMillisPart(uint16) whose argument matches timestampMillisPart.

Two failure paths are specified separately. Payload attributes that arrive malformed on forkchoiceUpdated MUST draw a JSON-RPC Invalid params error (-32602). A payload whose BaseTime deposit is missing or wrong MUST instead be reported invalid during newPayload validation.

Payload IDs fold in timestampMillisPart, so two builds that differ only by millisecond part get distinct IDs. Omit the field and the legacy ID calculation applies unchanged. Every Engine timestamp field otherwise stays in seconds.

The checks split across two moments, because headers carry no milliseconds.

On forkchoiceUpdated the execution client validates what it can without touching contract state: the millisecond part is absent before Denim, present and one of the five legal values after, and the whole-second timestamp is not behind the parent’s.

The 200ms spacing can only be confirmed once the block has executed and the deposit is readable. At that point the client MUST confirm the full timestamp lands exactly 200ms past the parent’s. A block that misses is invalid.

After activation, derivation computes timestamps from the Denim schedule and the absolute L2 block number. It reads the millisecond part neither from batch data nor from the local clock, which is what keeps the result deterministic. Blocks walk the second in five steps:

p (.000)child (.200)child (.400)child (.600)child (.800)child (.000 in the next second)

The pipeline takes that scheduled timestamp, splits it into the header’s seconds and the BaseTime millisecond part, and rebuilds the deposit at tx[1].

Each selected 200ms slot gets a full build and execution pass. What comes out is a whole block, not a fragment of one: its own state root and hash, its own receipts, its own Engine payload and forkchoice update, and its own walk from unsafe to safe to finalized.

Two deposits lead the transaction list — L1 information first at tx[0], BaseTime next at tx[1] — and user transactions, plus anything else that applies, follow them. By design the payload attribute, the deposit at tx[1], and the value written into the predeploy all describe the same planned millisecond part.

Existing integrations keep working because timestamp stays in Unix seconds. So does everything around it: what the EVM reports as block.timestamp, what eth_call sees, the timestamps that decide transaction validity, and the ones the Engine API carries. The internal timestampMillisPart field is never surfaced over RPC at all.

For sub-second updates after the rollout, read canonical blocks and subscriptions such as eth_subscribe("newHeads"). Flashblocks streams stop.

An optional timestampMs gets added to the responses below, encoded as a JSON-RPC quantity holding the full Unix timestamp in milliseconds:

  • eth_getBlockByHash
  • eth_getBlockByNumber
  • eth_getHeaderByHash
  • eth_getHeaderByNumber
  • eth_subscribe("newHeads")

A block at 42.200 seconds reports:

FieldValue
timestamp0x2a
timestampMs0xa4d8

Clients are expected to take timestampMs from authenticated BaseTime metadata instead of guessing it from the seconds field. Where a historical block body or its metadata has been pruned or is otherwise missing, the field is left out rather than approximated.

Mined transaction objects gain an optional blockTimestampMs from:

  • eth_getTransactionByHash
  • eth_getTransactionByBlockHashAndIndex
  • eth_getTransactionByBlockNumberAndIndex

Full transaction objects nested inside block responses behave the same way. Pending transactions omit the field, since they have no canonical block to take a timestamp from.

Mined logs gain the same optional blockTimestampMs when returned by:

  • eth_getLogs
  • eth_getFilterChanges
  • eth_getFilterLogs
  • eth_getTransactionReceipt
  • eth_getBlockReceipts
  • eth_subscribe("logs")
  • eth_subscribe("transactionReceipts")

Receipts place the field one level down, on each nested log, and never on the receipt object itself. Should a log be removed, it keeps the block timestamp and the provenance it was first given. When the originating block’s authenticated metadata cannot be read, the field is dropped instead of estimated.

Pruned or unprovenanced transaction and log data behaves the same way. Keeping all of these optional is what lets pre-Denim history, and clients lacking the body data, still be represented.

Foundry can carry an unknown block-level timestampMs through its AnyRpcBlock and OtherFields paths while EVM execution keeps working in seconds. Plain Alloy AnyRpcHeader discards unknown fields, so anything that needs Denim timestamps should reach for WithOtherFields<AnyRpcHeader> or a typed Base response instead. Anvil is not expected to attach BaseTime metadata to locally mined blocks in the first rollout.

Applications reading Flashblocks preconfirmations are the ones with work to do, because that stream goes away rather than changing shape. The replacement is the canonical block feed, which after Denim arrives at the same 200ms cadence the Flashblocks stream did — with the difference that each update is a sealed block rather than a preview of a pending one.

Code that only reads timestamp needs no change. Code that needs millisecond resolution reads timestampMs on blocks and headers, or blockTimestampMs on transactions and logs, and must tolerate the field being absent for older or pruned data.