AggregateVerifier
games by re-deriving and re-executing an L2 block range inside an AWS Nitro Enclave. The same
service backs both proposal creation and dispute nullification: callers (proposer or challenger)
submit a block range, the host collects witness data, the enclave verifies the range, and a randomly-generated key
held only inside the enclave signs the resulting journal.
The signature is self-validating onchain. TEEVerifier recovers the signer from each proposal and
checks it against TEEProverRegistry for the active game implementation’s TEE_IMAGE_HASH. A
signer from a different enclave image, or one that is no longer registered, cannot satisfy
verification. The Nitro hypervisor’s per-instance attestation binds the signer’s public key to a
specific PCR0, which the registrar certifies separately.
Responsibilities
A conforming TEE prover stack performs the following work:- Serve
prover_provefor proposal and dispute ranges over JSON-RPC. - Collect witness data from canonical L1, L1 beacon, and L2 RPCs on the host.
- Forward content-verified preimages to the enclave over vsock.
- Inside the enclave, re-derive and re-execute the L2 range and validate the claimed output root against the re-executed one before signing anything.
- Sign per-block journals and an aggregate journal with a secp256k1 key generated inside the enclave.
- Expose
enclave_signerPublicKeyandenclave_signerAttestationfor the registrar. - Optionally gate every request on registry signer validity to fail closed against deregistered enclaves.
- Support multi-enclave deployment on a single EC2 parent so different PCR0 images can run side-by-side across rotations.
Architecture
The service runs as two processes on a Nitro-capable EC2 parent:- A host binary (
base-prover-nitro-host) that terminates JSON-RPC, collects witness data over HTTP, and proxies requests to one or more enclaves. - An enclave binary (
base-prover-nitro-enclave) packed into an EIF that holds the signing key, exposes a vsock listener, and runs the proof pipeline.
u32 big-endian length + bincode payload) with a 5-minute read
timeout. The transport caps write chunks at 28 KiB to avoid a Linux kernel virtio_vsock SKB
corruption bug.
JSON-RPC Interface
The host exposes two namespaces on a single HTTP JSON-RPC listener, plus an HTTPGET /healthz
proxy that routes to the JSON-RPC healthz method.
| Method | Purpose |
|---|---|
prover_prove | Produce per-block and aggregate signed proposals for a block range. |
enclave_signerPublicKey | Return the 65-byte uncompressed secp256k1 public key for each enclave. |
enclave_signerAttestation | Return the COSE_Sign1 attestation document for each enclave. |
healthz / GET /healthz | Liveness, plus optional onchain signer validity (latching) when enabled. |
enclave_* calls are all-or-nothing across multiple enclaves: if any transport fails or any
enclave returns an error, the entire response fails. Callers register every signer together, so a
partial response would be unusable.
prover_prove Request
ProofRequest fields:
| Field | Meaning |
|---|---|
l1_head | L1 head block hash anchoring the derivation window. |
l1_head_number | L1 head block number. |
agreed_l2_head_hash | L2 block hash at the parent of the range. |
agreed_l2_output_root | Output root at the parent. Used as the starting state. |
claimed_l2_output_root | Claimed output root at the target. Trust-critical: the enclave only signs if re-execution matches it. |
claimed_l2_block_number | Target L2 block number (ending block of the range). |
proposer | L1 address that will submit the proof. Committed into the journal so onchain msg.sender must match. |
intermediate_block_interval | Sampling stride for intermediate roots in the aggregate proposal. |
image_hash | keccak256(PCR0) the caller expects. Currently informational; routing uses onchain signer validity. |
prover_prove Response
ProofResult::Tee contains:
| Field | Meaning |
|---|---|
aggregate_proposal | One Proposal covering the full range with sampled intermediate roots. |
proposals | Per-block Proposals in order, each chaining prev_output_root to the previous block’s root. |
Proposal:
| Field | Meaning | ||||
|---|---|---|---|---|---|
output_root | Output root at this proposal’s ending block. | ||||
signature | 65-byte secp256k1 ECDSA signature (`r | s | v) over keccak256(journal)`. | ||
l1_origin_hash | L1 head hash used during derivation. | ||||
l1_origin_number | L1 head block number. | ||||
l2_block_number | Ending L2 block number for this proposal. | ||||
prev_output_root | Output root before this proposal’s range. | ||||
config_hash | Per-chain config hash hardcoded into the enclave. |
prev_output_root is the request’s agreed_l2_output_root, whose intermediate_roots are
sampled at intermediate_block_interval, and whose ending_l2_block is the last block in the
range.
enclave_signerAttestation
Takes optionaluser_data and nonce byte arguments. Both are capped at 512 bytes by the NSM
hardware and rejected at the host RPC layer before the vsock call. The host returns one raw
COSE_Sign1 document per configured enclave, in the same order as enclave_signerPublicKey. The
registrar uses this endpoint to bind each enclave’s signer to a fresh attestation before
submitting it onchain.
Proof Pipeline
A singleprover_prove request flows host → vsock → enclave → host:
- Host:
ProverService::prove_blockconstructs aHostfrom the prover config, then callsHost::build_witnessto walk L1 EL, L1 beacon, and L2 EL and populate anOraclewith hash-keyed preimages. - Host:
NitroBackend::proveflattens the oracle into(PreimageKey, Vec<u8>)pairs andNitroTransport::provesends them over vsock as oneEnclaveRequest::Prove(...)frame. - Enclave:
Oracle::newcontent-verifies everyKeccak256- orSha256-keyed preimage so the stored value actually hashes to its key. - Enclave:
BootInfo::loadextracts the proposer, L1 head, agreed/claimed roots, intermediate-block interval, and chain ID from local preimages. - Enclave:
config_hash_for_chainlooks up a hardcoded per-chain config hash fromCONFIG_HASHES(computed at first access fromChainConfig::all()). Unknown chain IDs returnUnsupportedChainand refuse to prove. - Enclave: the proof prologue drives derivation and execution via
driver.execute_with_intermediates(). The epilogue’svalidate()is the trust-critical gate: it confirms the re-executed final output root matches theclaimed_l2_output_rootfrom the request. Signing only happens after this check passes. - Enclave: for each block result, build a
ProofJournalwith emptyintermediate_rootsand sign it; chainprev_output_rootthrough the loop. Then build and sign the aggregate journal with sampled intermediate roots. - Enclave: return
EnclaveResponse::Prove(ProofResult::Tee { aggregate_proposal, proposals }). - Host: return the result to the JSON-RPC caller, applying the configured proof request timeout (default 1740 s, ~29 minutes).
proposeOutputRoots and the aggregate signature satisfies AggregateVerifier. The challenger
uses only the aggregate signature, repacking it for nullify() via
ProofEncoder::encode_dispute_proof_bytes. The enclave neither knows nor cares which caller it is
serving.
Signed Journal
Each signature is computed assecp256k1.sign(keccak256(journal)) and serialized as 65 bytes
(r || s || v). The journal is packed (not ABI-encoded), 196 + 32·N bytes where N is the
number of intermediate roots:
N == 0 and startingL2Block == endingL2Block - 1. Aggregate proposals
have startingL2Block == firstBlock - 1, endingL2Block == lastBlock, and N == lastBlock / intermediate_block_interval.
teeImageHash is keccak256(PCR0) taken at enclave boot. It is embedded in every journal so a
signature recovered onchain transitively commits to the exact EIF measurement that produced it. In
local mode (no NSM, development and test only), teeImageHash is zero.
The signature v byte is encoded as the secp256k1 recovery id (0 or 1); callers normalize it
to the EIP-155 form they need before L1 submission.
Multi-Enclave Routing
--vsock-cid accepts one or more CIDs, so a single host process can attach to multiple enclaves
running on the same EC2 parent. Each CID is an independent vsock endpoint that can run a different
EIF — a different PCR0, a different tee_image_hash, and a different registered signer.
The CLI requires --tee-prover-registry-address whenever more than one CID is configured. Without
the registry there is no way to choose between enclaves deterministically, so multi-enclave
deployments are fail-closed-only.
Per-request routing iterates configured CIDs in order and picks the first enclave whose signer is
currently valid in TEEProverRegistry:
- Fetch the signer public key from the enclave (skip the transport if this fails).
- Call
isValidSigner(signer)onTEEProverRegistry. - If valid, route the request to this enclave. If not, log and continue.
- If no enclave in the list has a valid signer, fail the request with
NoValidSigner.
TEE_IMAGE_HASH during the overlap window;
after the registry switches to the new image hash only the new enclave’s signer is valid, and all
new requests route to it.
enclave_* calls fan out to every configured enclave so the registrar can register every signer
in one cycle.
Registration Gating and Health
When--tee-prover-registry-address is set, the host enables two registry-backed behaviors:
GET /healthzreturns healthy only after at least one enclave’s signer has been confirmed valid onchain. The health flag latches: once an enclave has been seen valid,/healthzcontinues to report healthy even if the registry RPC later fails or the signer is deregistered. This keeps load balancers stable across short outages.- Every
prover_proverequest consultsRegistrationChecker::select_valid_enclavebefore forwarding. A deregistered enclave, or one whose key fetch fails, is skipped. If no enclave is valid the request is rejected with JSON-RPC error code-32001.
/healthz returns healthy as long as the
server is running, and prover_prove routes to the first configured enclave.
Attestation
The signer key is generated inside the enclave at startup and never leaves the enclave process. TheServer::new_enclave constructor:
- Opens an NSM session (
nsm_init). - Reads PCR0 (48-byte SHA-384). Wrong length aborts startup.
- Computes
tee_image_hash = keccak256(PCR0)and stores it for inclusion in every signed journal. - Generates a secp256k1 ECDSA key with
NsmRng, which callsnsm_process_request(Request::GetRandom). - Logs the signer address (no key material).
enclave_signerAttestation. Each call:
- Opens a fresh NSM session.
- Calls
nsm_process_request(Request::Attestation { public_key, user_data, nonce }). - Returns the raw COSE_Sign1 bytes.
user_data/nonce, all signed by
the per-instance Nitro hypervisor key. Only PCR0 is consumed by this system — it is the value
bound into every signed journal via teeImageHash = keccak256(PCR0). See the
registrar spec for how attestations are verified and submitted onchain.
Service Lifecycle
The host startup sequence (ServerArgs::run):
- Parse CLI; initialize logging and metrics via
base_cli_utils. - Resolve the
RollupConfigand L1 chain config from--l2-chain-id. Fail on unknown chains. - Build one
NitroTransport::vsock(cid, 8000)per--vsock-cid. - Construct
NitroProverServer::new_multi(prover_config, transports, timeout)and, if--tee-prover-registry-addressis set, wrap withRegistrationHealthConfig. - Build a jsonrpsee HTTP server with a
/healthzproxy layer, mergeProverApiServer,EnclaveApiServer, and one of the healthz modules, and start the server. - Block on the server handle; exit on ctrl-C.
NitroEnclave::new):
Server::new()opens NSM, derivestee_image_hash, and generates the signer key.- Bind a
VsockListeneronVMADDR_CID_ANY:8000. - For each connection, spawn a handler that reads one framed
EnclaveRequest, dispatches toServer::prove,signer_public_key, orsigner_attestation, writes the response, and closes the connection.
- (Optional)
select_valid_enclavechooses a registered enclave. tokio::time::timeout(proof_request_timeout, enclave.service.prove_block(request)).- On timeout, return JSON-RPC
-32000with the offending L2 block number. - On error from the enclave, return JSON-RPC
-32000with the underlying error message.
RuntimeManager. The jsonrpsee server stops, in-flight
requests drain, and the runtime exits. The enclave has no graceful shutdown path; process
termination drops NSM file descriptors via Drop.
Operator Inputs
A TEE prover host needs:- L1 execution RPC URL.
- L1 beacon RPC URL.
- L2 execution RPC URL.
- L2 chain ID (used to select the rollup config and per-chain config hash).
- JSON-RPC listen address.
- One or more vsock CIDs, each backed by a Nitro Enclave running the prover EIF.
- Proof request timeout (default 1740 seconds).
- Logging filter and Prometheus metrics settings.
TEEProverRegistryaddress. Required when more than one vsock CID is configured. Enables registration-gated health and per-request signer validation.- Experimental witness endpoint flag for hosts that expose
debug_executePayload.
Safety Requirements
A TEE prover implementation must preserve these safety properties:- Generate the signing key inside the enclave from the NSM hardware RNG and never serialize it out of the enclave process.
- Validate the re-executed final output root against the request’s
claimed_l2_output_rootbefore any signing, and refuse to sign if the check fails. - Embed
tee_image_hash = keccak256(PCR0)in every signed journal so signatures bind to one EIF measurement. - Content-verify every hash-keyed preimage as it enters the enclave so derivation cannot consume preimages whose values do not match their keys.
- Refuse to prove for chain IDs not present in the hardcoded
CONFIG_HASHEStable. - Cap
user_dataandnonceat the NSM 512-byte limit at the host RPC boundary so oversize attestation requests cannot reach the enclave. - Serve at most one request per vsock connection and keep no mutable state between requests so a malformed request cannot influence a later one.
- When
--tee-prover-registry-addressis configured, fail closed on per-request signer validity and reject the request if no configured enclave’s signer is currently valid onchain.