---
title: "Denim 200ms Native Blocks"
description: "Proposed specification for Denim, the upgrade that makes every 200ms interval a full canonical Base block, with BaseTime supplying the sub-second timestamp."
source: https://basehub.org/specifications/denim-200ms-blocks/
---
import { Aside } from '@astrojs/starlight/components';

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.

<Aside type="caution">
This page tracks a draft. Upstream publishes it for feedback only and makes no commitment to ship it, on any schedule or in this shape. Denim runs on neither Sepolia nor Mainnet, no activation time is set, and the required client versions are still open. Treat nothing here as final until upstream marks it so, and do not build production plans on it.
</Aside>

## What Denim changes

Today [Flashblocks](/flashblocks/overview/) 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.

## Activation

| Network | Activation timestamp | Required client versions |
| --- | --- | --- |
| `sepolia` | `TBD` | `TBD` |
| `mainnet` | `TBD` | `TBD` |

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

## Execution

### 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_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.

### 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

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

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

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

### 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

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.

## RPC

<Aside>
The behavior in this section is planned for Denim. None of it is live on production endpoints.
</Aside>

### 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

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:

| 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

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.

### Log and receipt timestamps

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.

### 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

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.
