---
id: TIP-1096
title: Multi-block Tempo Imports for Zones
description: Allows Zones to catch up through bounded header-only blocks and process pending Tempo work at a recent checkpoint.
authors: Dankrad Feist @dankrad
status: Draft
related: Tempo Zones, TIP-1091, https://github.com/tempoxyz/zones/pull/981
protocolVersion: T13
---

# TIP-1096: Multi-block Tempo Imports for Zones

## Abstract

This TIP lets a non-genesis Zone block import an ordered, nonempty range of
consecutive Tempo headers. It defines two block modes:

1. a header-only recovery block that advances the Zone's Tempo checkpoint and
   performs no other execution; and
2. a full block that advances the checkpoint, processes every pending deposit
   and token enablement against the final imported Tempo state root, and may then
   execute user transactions and finalize a withdrawal batch.

Header-only blocks let a Zone recover from a long interruption without obtaining
historical recipient-specific Tempo state proofs. Pending deposits and token
enablements remain globally bounded and are processed atomically by a final full
block at a recent checkpoint.

## Motivation

A Zone may produce blocks less frequently than Tempo or stop temporarily. If a
Zone block can import only one Tempo header, the Zone must produce one block per
missed Tempo block before it can use current Tempo state.

A single multi-header call is not sufficient for arbitrarily long interruptions:
header decoding and validation consume gas linearly, and pending deposits and
token enablements share the same system-transaction budget. Splitting recovery
into ordinary full blocks would require historical Tempo state proofs at every
split point. Some of those proof keys depend on decrypted deposit recipients, so
an independent historical-proof service could not discover them without access
to sequencer secrets.

Header-only recovery blocks solve this downtime case. They authenticate and
store only header ancestry and cannot execute user-visible work. The final full
block processes the globally bounded backlog against its final, recent state
root, where the sequencer can discover and request all required proofs on demand.

---

# Specification

## Header Range

A header-only import contains between 1 and
`MAX_TEMPO_HEADERS_PER_ZONE_BLOCK` headers, inclusive. A full import contains
exactly one header.

```solidity
uint64 constant MAX_TEMPO_HEADERS_PER_ZONE_BLOCK = 1024;
```

Within this inclusive range, no particular header count is canonical. A producer
MAY import any number of currently available consecutive headers and does not
need to wait until `MAX_TEMPO_HEADERS_PER_ZONE_BLOCK` headers are available.
Peers MUST NOT reject an otherwise valid header-only block because a different
valid split of the same catch-up range was possible.

Each item is the canonical RLP encoding of a Tempo header:

```text
rlp([
    general_gas_limit,
    shared_gas_limit,
    timestamp_millis_part,
    inner,
    consensus_context?
])
```

where `inner` is the Ethereum-compatible header defined by the active Tempo
protocol version and `consensus_context` is the trailing optional field defined
by TIP-1031. The field MUST be absent before T4 and in the genesis header, and
MUST be present in every post-T4 non-genesis header. Implementations MUST decode
the complete fork-appropriate header and compute its hash from the complete
canonical RLP bytes, including `consensus_context` when present.

Except for the bootstrap import described below, let `(storedHash, storedNumber)`
be the Zone's current Tempo checkpoint. The first imported header MUST satisfy:

```text
header[0].parent_hash == storedHash
header[0].number      == storedNumber + 1
```

Every later header `i` MUST satisfy:

```text
header[i].parent_hash == hash(header[i - 1])
header[i].number      == header[i - 1].number + 1
```

After validation, the Zone stores the hash and number of the final header as its
single Tempo checkpoint. Intermediate headers authenticate ancestry only and do
not create separately readable checkpoints. `TempoBlockFinalized` is emitted
once for the final header and contains its hash, number, and state root.

An empty or oversized array, malformed encoding, number gap, duplicate number,
or parent-hash mismatch invalidates the Zone block.

## Block Modes

The canonical Zone genesis block contains no Tempo system transaction. Every
non-genesis Zone block MUST begin with exactly one of the following system calls.

### Header-only recovery block

```solidity
function advanceTempoHeaders(bytes[] calldata headers) external;
```

`advanceTempoHeaders` validates the header range and advances only
`TempoState.tempoBlockHash` and `TempoState.tempoBlockNumber`. A block beginning
with this call:

- MUST NOT contain other system or user transaction;
- MUST NOT read Tempo account or storage state;
- MUST NOT process deposits or token enablements;
- MUST NOT change the processed deposit or enablement cursors;
- MUST NOT execute or finalize withdrawals.

The executing Zone block's timestamp, including its millisecond component, MUST
be greater than or equal to the final header's timestamp. Intermediate header
timestamps do not need to match the Zone block. A final-header timestamp greater
than the Zone block's timestamp invalidates the block.

The block may cross deposits, token enablements, sequencer-set changes, and
leadership changes because it performs no operation governed by that historical
state. Its beneficiary MUST equal the catch-up leader defined below; it is not
re-evaluated against leaders effective for later headers in the imported range.

### Header-only canonicality

The catch-up leader for a header-only block is the historical leader effective
for the first Tempo header imported by that block. Every header-only block MUST
be produced by its catch-up leader and MUST set its beneficiary to that leader.
A leadership change crossed later in the same imported header range does not
change the catch-up leader for that block. A later header-only block derives its
catch-up leader independently from its own first imported header.

The final full block retains the existing leader rules. It MUST be produced by
the leader effective for the Tempo header imported by its `advanceTempo` call,
or by the leader selected by an active forced-recovery override. Therefore, if a
header-only block begins while leader A is effective and its range crosses to
leader B, A produces that block. A following header-only block beginning under
leader B is produced by B. If no forced recovery is active, B also MUST produce
the final full block when its imported header is governed by B.

A header-only chain is authorized only as part of a batch that ends with a block
that calls `advanceTempo()` to process incoming queues and does not contain any
further `advanceTempoHeaders()` calls that aren't followed by an `advanceTempo()` call.

The existing threshold settlement certificate commits to the Zone height and
`BlockTransition`, whose `nextBlockHash` transitively commits to every
header-only block in that batch, including their beneficiaries. Quorum members
MUST validate the complete block sequence, including the catch-up producer and
beneficiary rules, before signing. The header-only chain is not independently
settlement-canonical and MUST NOT be submitted as a standalone batch. It becomes
settlement-canonical only when `submitBatch` accepts the batch ending in its full
block and the batch's `prevBlockHash` matches the portal's settled tip. Once
accepted, portal tip continuity rejects every conflicting transition from that
tip.

### Full block

```solidity
function advanceTempo(
    bytes calldata header,
    QueuedDeposit[] calldata deposits,
    DecryptionData[] calldata decryptions,
    EnabledToken[] calldata enabledTokens
) external;
```

`advanceTempo` retains its existing behavior. Its single header MUST directly
extend the checkpoint left by any preceding header-only block and is passed as a
one-element array to `TempoState.finalizeTempo`. The call processes the complete
pending deposit and token-enablement suffix visible at that header's state root;
user transaction execution and withdrawal finalization are unchanged.

## Batch Settlement

Every settleable batch MUST end in a full block. A batch containing only
header-only blocks is invalid, as is any mixed batch whose final block is
header-only.

The final full block follows the existing operational settlement rules,
including `finalizeWithdrawalBatch`. The proof exposes exactly the next
withdrawal batch index, and the settlement attestation binds that index with the
existing batch outputs. The certificate's block transition commits transitively
to every preceding header-only block in the batch; neither the proof nor
`submitBatch` performs a separate leader or beneficiary check for those blocks.

## Bootstrap

`advanceTempoHeaders` MUST NOT be used while the stored Tempo checkpoint hash is
zero. The first non-genesis block after canonical Zone genesis MUST be a full
block.

For that first import, ordinary parent continuity is unavailable. The batch proof
MUST prove against the final imported state root that the portal exists and has a
nonempty sequencer configuration. The proof then installs the final header as the
initial Tempo checkpoint and processes all portal work visible at that root.

## Outstanding Deposit Capacity

Each portal enforces:

```solidity
uint64 constant MAX_UNPROCESSED_DEPOSITS = 230;
uint64 constant WITHDRAWAL_BOUNCEBACK_RESERVE = 20;
```

Before appending an item, the portal computes:

```text
unprocessed = depositCount - lastProcessedDepositNumber
```

An ordinary deposit is rejected when `unprocessed` has reached
`MAX_UNPROCESSED_DEPOSITS - WITHDRAWAL_BOUNCEBACK_RESERVE`. A withdrawal
bounce-back is rejected when `unprocessed` has reached
`MAX_UNPROCESSED_DEPOSITS`.

Before processing a withdrawal batch, the portal conservatively assumes that
every withdrawal fails and produces a bounce-back. With:

```text
remainingCapacity = MAX_UNPROCESSED_DEPOSITS - unprocessed
```

the call rejects `withdrawals.length > remainingCapacity` before processing any
withdrawal. A failed call leaves both queues unchanged.

The value 230 is the measured upper bound for processing the worst-case deposit
queue within 200,000,000 gas. The 20 reserved entries cover one maximum-size
withdrawal batch under the existing Zones limits.

The header, deposit, and token bounds are not independent gas allowances. The
combined worst case of one imported header,
`MAX_UNPROCESSED_DEPOSITS` deposits, and
`MAX_UNPROCESSED_TOKEN_ENABLEMENTS` token enablements MUST fit within the full
block's 225,000,000 gas ceiling. The measured worst case of 230 deposits and 8
maximum-metadata enablements uses 214,832,282 gas. Recovery remains live at that
combination because the sequencer can use header-only blocks until exactly one
header remains for the full block.

## Outstanding Token Enablement Capacity

Token enablement changes from a per-Tempo-block limit to a global outstanding
limit:

```solidity
uint64 constant MAX_UNPROCESSED_TOKEN_ENABLEMENTS = 8;
```

The portal maintains:

```text
enabledTokenCount
lastProcessedEnabledTokenCount
```

Before enabling a token, it requires:

```text
enabledTokenCount - lastProcessedEnabledTokenCount
    < MAX_UNPROCESSED_TOKEN_ENABLEMENTS
```

The existing append-only `_enabledTokens` array defines canonical enablement
order. A full `advanceTempo` MUST process the complete unprocessed suffix visible
at its final root. The proof authenticates the supplied token addresses, order,
and metadata by extending `ZoneInbox.processedTokenEnablementHash` with each
enablement and requiring the resulting hash to equal
`ZonePortal.tokenEnablementHash` at the final root. Policy state used for Zone
token initialization is verified against the same root.

The Zone stores `processedEnabledTokenCount`. A full block starts at that Zone
count, not at the portal's last-settled count, and processes every array entry up
to `enabledTokenCount` at the final root. This prevents replay when several full
blocks execute before their containing batch is submitted.

The batch proof exposes the processed-count transition explicitly:

```solidity
struct TokenEnablementTransition {
    uint64 prevProcessedTokenCount;
    uint64 nextProcessedTokenCount;
}
```

Counts are sufficient because `_enabledTokens` is append-only, so each count
uniquely identifies a prefix. Unlike the deposit queue, the token sequence does
not require processed-prefix hashes.

Except during activation initialization, `submitBatch` requires:

```text
transition.prevProcessedTokenCount == lastProcessedEnabledTokenCount
transition.prevProcessedTokenCount <= transition.nextProcessedTokenCount
transition.nextProcessedTokenCount <= enabledTokenCount
```

The proof requires the Zone pre-state `processedEnabledTokenCount` to equal
`prevProcessedTokenCount`, verifies every newly processed array entry in order,
and requires the Zone post-state count to equal `nextProcessedTokenCount`. For
each full block, the resulting Zone count MUST equal `enabledTokenCount` at that
block's final imported Tempo root. A header-only block advances neither count
and contributes an identity step to its containing batch transition.

After accepting the proof, `submitBatch` sets
`lastProcessedEnabledTokenCount` to `nextProcessedTokenCount`. Skipping,
reordering, replaying, or partially processing a suffix is invalid.

When T13 replaces the T12 portal runtime, the new
`tokenEnablementCursorInitialized` storage slot is false. This flag serves as the
in-contract T13 activation gate for the new token cursor; it does not disable
unrelated T13 functionality. While it is false, the portal MUST reject new token
enablements. Header-only Zone blocks MAY advance the local checkpoint during
recovery, but they are not settlement boundaries. They may only form a prefix of
the first post-activation batch, which MUST end in a full block and use
operational settlement.

The first post-activation proof supplies a count `N` and authenticates the Tempo
header stored in the parent Zone state. Against that header's state root, it MUST
prove both:

```text
ZoneInbox.processedTokenEnablementHash == ZonePortal.tokenEnablementHash
ZonePortal.enabledTokenCount            == N
```

The activation state transition internally derives `N` and initializes the
Zone's `processedEnabledTokenCount` to `N` before executing the first block. For
this one batch, `TokenEnablementTransition.prevProcessedTokenCount` MUST remain
the physical pre-state value `0`; the proof authenticates `N` internally before
processing the outstanding suffix. The verifier treats an uninitialized portal
cursor as the activation case instead of requiring ordinary count continuity.

The proof MUST additionally verify against the full block's final imported Tempo
root that both outstanding-work bounds hold:

```text
ZonePortal.depositCount
    - DepositQueueTransition.prevDepositNumber
    <= MAX_UNPROCESSED_DEPOSITS

ZonePortal.enabledTokenCount
    - N
    <= MAX_UNPROCESSED_TOKEN_ENABLEMENTS
```

For this activation batch, `N` is the effective previous processed-token count
for the outstanding-work bound even though
`TokenEnablementTransition.prevProcessedTokenCount` remains the physical Zone
pre-state value `0`. Subsequent batches use the transition's ordinary previous
count. The activation batch's next count includes `N` and the complete suffix
processed by its final full block.

Only after accepting a proof that satisfies these checks does `submitBatch` set
`lastProcessedEnabledTokenCount` to the transition's next count and set
`tokenEnablementCursorInitialized` to true. This prevents replaying initialized
tokens, treating post-checkpoint enablements as processed, or completing cursor
initialization with more pending work than one full block can process.

## Interface and Proof Changes

`IZoneInbox` adds:

```solidity
function advanceTempoHeaders(bytes[] calldata headers) external;
```

`ITempoState.finalizeTempo` changes the signature to accept multiple headers:
```solidity
function finalizeTempo(bytes[] calldata headers) external;
```

`IZonePortal.MAX_DEPOSITS_PER_TEMPO_BLOCK()` is replaced by
`MAX_UNPROCESSED_DEPOSITS()`.

`IZonePortal.MAX_UNPROCESSED_TOKEN_ENABLEMENTS()` and
`lastProcessedEnabledTokenCount()` are added. Portal storage also adds
`tokenEnablementCursorInitialized()`, which records whether the first operational
proof has initialized the token cursor and proven both outstanding-work bounds.

`ZonePortal.submitBatch` and `IVerifier.verify` add a
`TokenEnablementTransition` parameter. The batch proof returns this transition as
a public output. The settlement attestation commits to
`keccak256(abi.encode(tokenEnablementTransition))`, analogous to its existing
deposit-transition commitment, so a certificate cannot be reused with different
token counts.

### Events

T13 extends `ZoneInbox.TempoAdvanced` with the cumulative token-enablement cursor
after the full import:

```solidity
event TempoAdvanced(
    bytes32 indexed tempoBlockHash,
    uint64 indexed tempoBlockNumber,
    uint256 depositsProcessed,
    bytes32 newProcessedDepositQueueHash,
    uint64 lastProcessedDepositNumber,
    uint64 lastProcessedEnabledTokenCount
);
```

`lastProcessedEnabledTokenCount` MUST equal the post-execution value of
`ZoneInbox.processedEnabledTokenCount`. It is cumulative. After activation has
initialized the cursor, the number of token enablements processed by a call is
the difference from the preceding full block's value. The first post-activation
value also incorporates the authenticated migration prefix `N` described above.
A full import emits `TempoAdvanced` even when neither deposits nor token
enablements were processed.

T13 likewise extends `ZonePortal.BatchSubmitted` with the cumulative cursor
accepted by settlement:

```solidity
event BatchSubmitted(
    uint64 indexed withdrawalBatchIndex,
    uint256 indexed withdrawalQueueIndex,
    bytes32 nextProcessedDepositQueueHash,
    bytes32 nextBlockHash,
    bytes32 withdrawalQueueHash,
    uint64 lastProcessedDepositNumber,
    uint64 lastProcessedEnabledTokenCount
);
```

`lastProcessedEnabledTokenCount` MUST equal the portal's post-settlement value
and the accepted `TokenEnablementTransition.nextProcessedTokenCount`.

`NO_QUEUE_INDEX` means that the settlement created no withdrawal queue entry.
Consumers MUST NOT create or overwrite a withdrawal queue record for such an
event. This occurs when the final full block finalizes an empty withdrawal batch;
the `withdrawalBatchIndex` still advances exactly once.

Before the coordinated T13 activation, `TempoAdvanced` and `BatchSubmitted`
retain their legacy signatures without `lastProcessedEnabledTokenCount`.
Because appending a field changes an event's signature hash, indexers and
settlement tooling that span the activation boundary MUST recognize both the
legacy and post-activation signatures and select the decoder by topic.

No other event signature changes. `advanceTempoHeaders` emits no
`TempoAdvanced`; its only checkpoint event is the existing single
`TempoBlockFinalized` for the final imported header. A header-only block emits
no `BatchFinalized` and is not a settlement boundary. The final full block emits
`BatchFinalized`, and settlement of the batch ending in that block emits the
post-activation `BatchSubmitted` described above. Deposit, withdrawal, and
token-enablement lifecycle event signatures remain unchanged.

## Activation and Compatibility

This TIP activates through the coordinated T13 Tempo upgrade and Zone hardfork.
T13 installs the new `ZonePortal` runtime and activates the new Zone
block and execution rules. Implementations MUST coordinate both activation
boundaries and MUST NOT produce, prove, or settle TIP-1096 blocks while only one
side is active.

The activation updates `ZonePortal`, `ZoneInbox`, `TempoState`, the Zone block
witness, state-transition function, and verifier atomically.
Existing Zone checkpoint and deposit state are retained. Token-enablement cursors
are initialized as specified above.

Before installing T13, operators MUST ensure that the last T12-settled Zone state
can satisfy the initialization bounds. If either pre-existing backlog exceeds its
new global limit, the backlog MUST be processed and settled under T12 before the
upgrade; T13 deliberately cannot initialize the new token cursor from an
oversized backlog.

The existing `IZoneInbox.advanceTempo(bytes,...)` ABI remains unchanged.
`IZoneInbox.advanceTempoHeaders(bytes[])` is new, and
`ITempoState.finalizeTempo(bytes)` is replaced by
`ITempoState.finalizeTempo(bytes[])`. The block witness, settlement proof
statement, settlement attestation, and portal storage also change. Sequencers,
provers, verifiers, RPC encoders, block decoders, and canonical runtimes MUST
switch at the same activation boundary. Pre-activation blocks retain their
active protocol-version rules.

# Tooling

Payload builders and block decoders MUST represent the opening Tempo system
transaction as an explicit header-only or full variant. Inferring the mode from
empty deposit arrays is invalid because a full block may legitimately process no
deposits or token enablements.

During recovery, the sequencer MUST:

1. reserve one finalized Tempo header for the next full block;
2. split every earlier consecutive header into bounded header-only blocks;
3. retain those header-only blocks as an unsettled prefix of the next batch;
4. collect the complete deposit and token-enablement suffix since the last full
   block; and
5. decrypt deposits and obtain Tempo state proofs only for the single header
   imported by the full block, then prove and submit the batch ending in that
   full block.
