---
id: TIP-1098
title: Nitro-Backed Zone Batch Verification
description: Defines the native T13 verifier for Nitro-attested Zone batch proofs.
authors: Roman Krasiuk, Zygis
status: Draft
related: TIP-1091, [Tempo Zones spec](https://github.com/tempoxyz/zones/blob/main/specs/spec.md), [AWS Nitro Attestation Process](https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md)
protocolVersion: T13
---

# TIP-1098: Nitro-Backed Zone Batch Verification

## Abstract

The Zone portal activates at T10 with a permissive verifier placeholder. This TIP activates a
native verifier at T13, accepting only batches executed by the approved Zone stateless proof
function inside AWS Nitro Enclaves. The verifier authenticates the Nitro document directly, pins
the measured enclave image, and requires the signed `user_data` to commit to every public input
used by `IVerifier`. No separate, application-facing Nitro attestation precompile is introduced.

## Motivation

Zone portals release withdrawals and advance their canonical state only after calling the shared
verifier at `0x5a56000000000000000000000000000000000000`. The T10 verifier is intentionally
permissive while proof production is brought online. Leaving that behavior in place would let an
authorized sequencer settle an arbitrary state transition.

Validating Nitro attestations requires bounded CBOR and COSE decoding, an X.509 chain anchored to
AWS, SHA-384 hashing, and P-384 signatures. The T13 native verifier performs those operations and
the Zone-specific PCR, freshness, and batch-binding policy atomically.

## Assumptions

- The AWS Nitro platform and commercial-partition Attestation PKI are trusted. The canonical root
  is embedded in the verifier; rotating it requires a hardfork.
- Revocation checking is not performed. Certificates are instead required to be valid at the
  current Tempo block timestamp.
- The measured enclave application is trusted exactly as identified by PCR0, PCR1, and PCR2. It
  must not expose an endpoint that attests a caller-selected digest.

## Threat Model

- Enclave operators, sequencers, and transaction submitters are untrusted. They may replay or
  corrupt documents but cannot produce an accepted signature for a different measured image or
  batch commitment.
- Portal transition and batch-index checks prevent reuse after the portal advances.

---

# Specification

## Activation and Interface

At T13, native dispatch MUST take precedence at the protocol-managed verifier address. The T10
runtime remains observable account code and continues to execute before T13. No additional
precompile address or runtime installation is defined.

The verifier retains the proof-agnostic interface extended by TIP-1096:

```solidity
function verify(
    uint32 zoneId,
    uint64 tempoBlockNumber,
    uint64 anchorBlockNumber,
    bytes32 anchorBlockHash,
    uint64 expectedWithdrawalBatchIndex,
    uint256 nextZoneHeight,
    BlockTransition calldata blockTransition,
    DepositQueueTransition calldata depositQueueTransition,
    TokenEnablementTransition calldata tokenEnablementTransition,
    bytes32 withdrawalQueueHash,
    bytes calldata verifierConfig,
    bytes calldata proof
) external view returns (bool);
```

`verifierConfig` MUST be the single byte `0x01`. `proof` MUST be the raw COSE/CBOR Nitro
attestation document.

After charging input gas, calls with the `verify` selector and total calldata length exceeding
25,220 bytes MUST return `false` before ABI decoding, without Nitro base or signature charges.
All other calls follow normal precompile ABI dispatch: missing or unknown selectors and malformed
arguments revert. Successfully decoded calls return `false` for a noncanonical caller, unsupported
configuration, empty or invalid proof, policy failure, or commitment mismatch. Out-of-gas remains
an exceptional halt.

For successfully decoded calls, `msg.sender` MUST equal the canonical portal address for `zoneId`
as defined by TIP-1091. The verifier MUST reject any other caller before document parsing. This
check binds the portal domain through `zoneId`, so the portal address is not a separate commitment
field.

## Nitro Document Validation

All Nitro document validation is internal to `verify` and MUST complete before PCR, freshness,
and commitment policy is applied.

1. The document is limited to 24,576 bytes and decodes, with no trailing bytes, as a definite-length
   four-element `COSE_Sign1` array `[protected, unprotected, payload, signature]`. A single tag 18
   MAY wrap this array; no other CBOR tags are accepted. All CBOR byte and text strings MUST use
   definite-length encoding; chunked strings are rejected.
2. The protected-header byte string MUST contain exactly the definite-length map `{1: -35}` with
   no trailing bytes. The payload is a 1–16,384-byte string and the ES384 signature is exactly
   96-byte `r || s`. The unprotected header MUST be an empty map, which MAY use definite- or
   indefinite-length encoding.
3. The payload is a CBOR map containing non-null, non-duplicate `module_id`, `digest`, `timestamp`,
   `pcrs`, `certificate`, and `cabundle`, with no trailing bytes. `module_id` is nonempty text;
   `digest` is `SHA384`; timestamp is a nonzero `uint64` in milliseconds. Field names MUST be text
   strings; unknown fields and duplicate fields are rejected, including duplicate optional fields.
   Optional `public_key`, `user_data`, and `nonce` retain their AWS meanings; they MAY be absent or
   CBOR null, which is represented internally as empty bytes. CBOR undefined is rejected.
4. PCR indices are unique integers from 0 through 31, with 1–32 total entries and values of 32,
   48, or 64 bytes. The CA bundle contains 1–32 DER certificates of at most 1,024 bytes each.
   `certificate` MUST be a CBOR byte string of 1–1,024 bytes. This bound MUST be checked during
   parsing, before signature gas is charged.
   A non-null `public_key` is 1–1,024 bytes; `user_data` and `nonce` are at most 512 bytes.
   The payload map, PCR map, and CA bundle array MAY use definite- or indefinite-length encoding.
   Nested values outside the specified field types are rejected; arbitrary CBOR trees are not
   traversed.
5. `cabundle` is ordered `[ROOT, INTERMEDIATE_1, …, INTERMEDIATE_N]`; the document certificate is the leaf.
   The first entry MUST byte-match the embedded AWS root. Every certificate MUST be strict DER,
   X.509 v3, ECDSA-with-SHA384, and use an uncompressed P-384 key.
6. Every validity interval contains the current block timestamp. Issuer and subject names link at
   each step. Root and intermediates are CAs with `keyCertSign`; path-length limits count only
   non-self-issued intermediate CAs as required by RFC 5280. The leaf is not a CA and has
   `digitalSignature`. Unknown critical extensions are rejected.
7. Every non-root certificate signature verifies under its parent. The COSE signature verifies
   under the leaf over RFC 9052's exact `Sig_structure`. High- and low-s signatures are accepted.

## Zone Policy

The attestation MUST contain the following 48-byte SHA-384 measurements:

| Register | Required value |
|---|---|
| PCR0 | `<T13_PCR0_SHA384>` |
| PCR1 | `<T13_PCR1_SHA384>` |
| PCR2 | `<T13_PCR2_SHA384>` |

Until these release measurements are finalized, the production verifier has no approved tuple and
MUST return `false` after attestation validation. The tuple is a hardfork constant and is not
runtime-configurable. Other locked PCRs MAY be present.

The document timestamp MUST NOT exceed `block.timestamp * 1000 + 300_000`; no additional maximum
age is imposed. `user_data` MUST be exactly 32 bytes and equal:

```text
keccak256(abi.encode(
    keccak256("NitroBatchAttestation(uint256 parentChainId,address verifier,uint32 zoneId,uint64 tempoBlockNumber,uint64 anchorBlockNumber,bytes32 anchorBlockHash,uint64 expectedWithdrawalBatchIndex,uint256 nextZoneHeight,bytes32 prevBlockHash,bytes32 nextBlockHash,bytes32 prevProcessedHash,bytes32 nextProcessedHash,uint64 prevDepositNumber,uint64 nextDepositNumber,uint64 prevProcessedTokenCount,uint64 nextProcessedTokenCount,bytes32 withdrawalQueueHash,bytes32 verifierConfigHash)"),
    block.chainid,
    address(this),
    zoneId,
    tempoBlockNumber,
    anchorBlockNumber,
    anchorBlockHash,
    expectedWithdrawalBatchIndex,
    nextZoneHeight,
    blockTransition.prevBlockHash,
    blockTransition.nextBlockHash,
    depositQueueTransition.prevProcessedHash,
    depositQueueTransition.nextProcessedHash,
    depositQueueTransition.prevDepositNumber,
    depositQueueTransition.nextDepositNumber,
    tokenEnablementTransition.prevProcessedTokenCount,
    tokenEnablementTransition.nextProcessedTokenCount,
    withdrawalQueueHash,
    keccak256(verifierConfig)
))
```

## Gas

The verifier charges `30 * ceil(calldata.length / 32)` gas before ABI decoding. This covers the
entire calldata, including the function selector and ABI padding. A noncanonical caller, unsupported
configuration, empty proof, and proof length above 24,576 bytes return `false` without Nitro base or
signature gas.

Calls that reach document parsing pay 40,000 gas immediately before parsing. After successful
parsing determines the chain length, the verifier charges `35,000 * (cabundle.length + 1)` gas
before certificate validation or any P-384 verification. This counts every non-root certificate
signature, including the leaf, plus the COSE signature; the pinned root is not self-verified.
Validation failures retain charges already incurred. Insufficient gas remains an exceptional halt.

# Tooling

Consensus-critical CBOR/COSE and X.509 validation lives in the internal, `no_std`
`tempo-nitro-attestation` crate. It uses pinned `minicbor` and `minicbor-serde` decoders with borrowed
fields and explicit size and collection bounds; the Zone verifier supplies AWS-LC SHA-384 and
P-384 operations. The crate exposes structured errors for tests and tooling, while the onchain
verifier maps every non-gas validation failure to `false`.

The repository vendors a fixed-time production AWS attestation, tests high-s acceptance and
deterministic certificate mutations, and provides a differential fuzz target against
`x509-parser` and `rustls-webpki`. The precompile benchmark compares the five-signature Nitro path
with the existing P-256 verifier for gas calibration.

The measured application selects a trusted Tempo chain specification by request chain ID, executes
the Zone SPF to completion, derives the digest above from the public inputs and output, and only
then asks the Nitro Security Module to place it in `user_data`. It MUST reject caller-provided
digests, configuration, SPF outputs, arbitrary attestation payloads, chain mismatches, SPF failure,
or NSM failure. Production sequencers MUST NOT submit an empty proof as fallback.

# Observability

N/A. The verifier is a deterministic view function and emits no events. Existing transaction
traces expose the verifier address, calldata, gas use, return value, and exceptional halt needed to
distinguish policy rejection from out-of-gas.

# Invariants

| ID | Invariant | Description |
|---|---|---|
| ZV1 | Chain of trust | Every accepted proof chains to the embedded AWS root. |
| ZV2 | Measured execution | Every accepted proof contains the approved PCR0/1/2 tuple. |
| ZV3 | Complete binding | Changing any public batch input, configuration, chain, or verifier changes `user_data`; only the canonical portal for the committed `zoneId` can obtain `true`. |
| ZV4 | Fail closed | Unsupported configuration and all proof or policy errors return false. |
| ZV5 | Bounded resources | Input, collections, and fields obey the stated limits and CBOR type restrictions. |
| ZV6 | Gas before work | Parsing and signature gas are charged before their respective work. |
| ZV7 | Release consistency | The TIP, verifier constants, and canonical EIF identify the same PCR tuple. |
