---
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: T11, Z1
---

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

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 final header's timestamp and millisecond component MUST exactly match the
executing Zone block's timestamp and millisecond component. Intermediate header
timestamps do not need to match the Zone block. A mismatch in either final-header
component 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 is not checked against the historical or final-root
leader.

### Header-only canonicality

Header-only block validity does not bind the block beneficiary or producer to a
Tempo leader. No historical or current Tempo leader is designated for a
header-only block, and leadership changes within or after its imported range do
not affect its validity.

A header-only chain is authorized through the existing threshold settlement
certificate. The certificate commits to the Zone height and `BlockTransition`,
whose `nextBlockHash` transitively commits to every header-only block, including
its beneficiary. Quorum members MUST validate the complete header-only block
sequence before signing, but they do not verify a Tempo-derived producer for
those blocks. The certificate therefore selects an exact transition rather than
attesting that a particular Tempo leader produced it.

Any active sequencer may construct, propose, relay, or submit the certified
transition. A candidate header-only chain is not canonical merely because of its
sender. It becomes canonical when `submitBatch` accepts its threshold certificate
and its `prevBlockHash` matches the portal's settled tip. Once accepted, portal
tip continuity rejects every conflicting transition from that tip.

This rule deliberately makes header-only coordination depend on the configured
settlement threshold. In a one-of-one configuration, the sole sequencer chooses
the canonical header-only chain. Full blocks retain the existing Tempo leader
rules.

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

Settlement behavior is derived from the proven block sequence. A settleable batch
either contains only header-only blocks or ends in a full block. A batch that
contains a full block but does not end in one is invalid.

A header-only batch exposes identity deposit and `TokenEnablementTransition`
values. Its public withdrawal queue hash is zero, and the verifier checks that
`ZoneOutbox.lastBatch` is unchanged instead of copying that storage value into
the public withdrawal output. The portal MUST NOT increment
`withdrawalBatchIndex`; it updates only its settled Zone block hash,
`lastSyncedTempoBlockNumber`, and Zone height.

Its settlement certificate authorizes the exact header-only block transition as
specified above; neither the proof nor `submitBatch` performs a separate leader
or beneficiary check for that transition.

A batch ending in a full block follows the existing operational settlement rules,
including `finalizeWithdrawalBatch` in its final block. The proof exposes the
resulting withdrawal batch index: it equals the portal's current index for a
header-only batch and exactly the next index for an operational batch. The
settlement attestation binds this index with the existing batch outputs; any
other index or block shape is invalid.

## 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 verifies the supplied token addresses against that
array in order and verifies the metadata and policy state used for Zone token
initialization 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 a header-only batch therefore exposes an identity transition.

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

When T11 replaces the T10 portal runtime, the new
`tokenEnablementCursorInitialized` storage slot is false. This flag serves as the
in-contract T11 activation gate for the new token cursor; it does not disable
unrelated T11 functionality. While it is false, the portal MUST reject new token
enablements and MUST reject settlement of a header-only batch. The first
post-activation batch MUST use operational settlement, though it MAY contain
header-only recovery blocks before its final full block.

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
    - TokenEnablementTransition.prevProcessedTokenCount
    <= MAX_UNPROCESSED_TOKEN_ENABLEMENTS
```

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.

`ZonePortal.submitBatch` adds a `resultingWithdrawalBatchIndex` parameter and
passes it to the verifier. The existing settlement-attestation field binds that
value. The portal accepts only its current index or exactly the next index, the
verifier checks that the value matches the proven batch shape, and successful
settlement assigns the portal index to that value instead of unconditionally
incrementing it.

## Activation and Compatibility

This TIP activates through the coordinated T11 Tempo upgrade and Z1 Zone
hardfork. T11 installs the new `ZonePortal` runtime and Z1 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 T11, operators MUST ensure that the last T10-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 T10 before the
upgrade; T11 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. optionally prove and submit one or more checkpoint batches;
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.
