Denim 200ms Native Blocks
Denim changes 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.
What Denim changes
Section titled “What Denim changes”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.
Network availability
Section titled “Network availability”| Network | Status | Activation timestamp | Required client versions |
|---|---|---|---|
vibenet | Live for experimental testing | — | — |
sepolia | Planning for October 2026 | TBD | TBD |
mainnet | Planning for October 2026 | TBD | TBD |
Vibenet carries 200ms blocks today, which makes it the place to exercise an integration ahead of time.
Upstream lists October 2026 as the planning target for Denim on both Sepolia and Mainnet. That is a target under a planning status rather than a booked activation: neither network has a published timestamp, and the required client versions are still open. The node source is consistent with that, leaving denim_timestamp unset on every canonical network. Treat the month as subject to change until timestamps appear.
The fork name is no longer in doubt. Upstream briefly filed 200ms blocks under Cobalt in early September 2026 and has since moved them back to Denim, which matches what the node has done throughout: every block-timing path gates on is_denim_active_at_timestamp, and the BaseTime helper lives under the same gate.
Block-number contract logic
Section titled “Block-number contract logic”Audit anything that converts a block count into a duration before activation. The common patterns and what each does afterwards:
| Pattern | Effect after Denim |
|---|---|
Timelocks, cooldowns, and exit delays written as block.number - start >= N | Elapse in one tenth of the intended wall-clock time. |
| Vesting, emission, or difficulty schedules counted in blocks | Complete or retarget ten times faster in wall-clock time. |
Per-block rate limits or caps that reset whenever block.number moves | Reset five times per second rather than once every two seconds, lifting the effective per-second limit ten times. |
Deadlines set as expirationBlock = block.number + N | Expire in one tenth of the intended wall-clock time. |
Governance voting delay and voting period in blocks, including ERC20Votes on the default block-number clock() | Voting windows shrink ten times. Stored checkpoints stay correct. |
| Uniswap V3 style TWAP oracles | The averages stay correct, because the accumulators weight by seconds rather than by blocks. The catch is history depth: a fixed observation cardinality now spans roughly half the wall-clock window it used to, so a long observe() lookback starts reverting with OLD earlier than it did. |
Logic that measures time with block.timestamp is unaffected, which is the cheapest fix where a redeploy is possible.
Four ways to prepare, roughly in order of preference:
- Swap block-count durations for
block.timestampcomparisons anywhere you can redeploy or upgrade. - Where the block count is governance-tunable, queue a change that divides it by ten at activation.
- Where the count is immutable and cannot be changed, work out the blast radius now and plan a migration rather than discovering it on activation day.
- Exercise the new behaviour on vibenet, which already seals 200ms blocks.
Execution
Section titled “Execution”Full block timestamp
Section titled “Full block timestamp”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_partOnly 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) + 200Slots 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.
The BaseTime metadata deposit
Section titled “The BaseTime metadata deposit”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.
| Field | Value |
|---|---|
| Transaction type | Deposit (0x7e) |
| Source hash | Domain 3, bound to the current block number |
| From | 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001 |
| To | 0x4200000000000000000000000000000000000030 |
| Mint | 0 |
| Value | 0 |
| Gas limit | 1,000,000 |
| System transaction | false |
| Calldata | setTimestampMillisPart(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.
The BaseTime predeploy
Section titled “The BaseTime predeploy”Contracts that need the current millisecond part read it from a predeploy.
| Property | Value |
|---|---|
| Proxy | 0x4200000000000000000000000000000000000030 |
| Implementation | 0xc0D3C0d3C0d3C0D3c0d3C0d3c0D3C0d3c0d30030 |
| Storage | uint16 millisecond part in slot 0 |
| Setter | setTimestampMillisPart(uint16) |
| Millisecond-part getter | timestampMillisPart() |
| Full-timestamp getter | timestampMs() |
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.
Engine payload attributes
Section titled “Engine payload attributes”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.
Validation
Section titled “Validation”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.
Derivation
Section titled “Derivation”Scheduled timestamps
Section titled “Scheduled timestamps”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].
Block lifecycle
Section titled “Block lifecycle”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.
Seconds stay put
Section titled “Seconds stay put”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.
Block and header timestamps
Section titled “Block and header timestamps”An optional timestampMs gets added to the responses below, encoded as a JSON-RPC quantity holding the full Unix timestamp in milliseconds:
eth_getBlockByHasheth_getBlockByNumbereth_getHeaderByHasheth_getHeaderByNumbereth_subscribe("newHeads")
A block at 42.200 seconds reports:
| Field | Value |
|---|---|
timestamp | 0x2a |
timestampMs | 0xa4d8 |
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.
Transaction timestamps
Section titled “Transaction timestamps”Mined transaction objects gain an optional blockTimestampMs from:
eth_getTransactionByHasheth_getTransactionByBlockHashAndIndexeth_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.
Log and receipt timestamps
Section titled “Log and receipt timestamps”Mined logs gain the same optional blockTimestampMs when returned by:
eth_getLogseth_getFilterChangeseth_getFilterLogseth_getTransactionReceipteth_getBlockReceiptseth_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.
Tooling
Section titled “Tooling”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.
What this means for integrators
Section titled “What this means for integrators”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.