TEE Prover Registrar
The registrar is an offchain service that keeps the onchain set of accepted TEE signer identities in sync with the actual running prover fleet. It locates live TEE prover instances, pulls each enclave’s AWS Nitro attestation document, computes the P-384 inverse hints the onchain verifier needs to check it, and writes the resulting signer registration to TEEProverRegistry on L1. It also removes signers whose backing instances have disappeared and revokes certificates that AWS has marked as withdrawn.
The Base team operates a single registrar. The proof system trusts only signers that this registrar has admitted, which makes registrar correctness a prerequisite for accepting TEE proofs onchain. The output is still self-validating end-to-end. Every certificate signature, the attestation signature, the enclave PCR0 measurement, and the signer public key are re-checked onchain by TEEProverRegistry and its NitroValidator and CertManager before the signer is treated as valid. The hints only speed that check up. A wrong hint makes the check fail, it cannot make a bad attestation pass.
Responsibilities
Section titled “Responsibilities”A conforming registrar performs the following work each cycle:
- Discover the current set of TEE prover instances behind the production load balancer.
- Fetch per-enclave signer public keys and Nitro attestation documents from each instance.
- Check the attestation certificate chain against the onchain revocation set and, when enabled, against AWS-published CRLs.
- Compute P-384 hints for every enclave that is not yet registered, and cache any certificate in the chain that
CertManagerhas not verified yet. - Submit
TEEProverRegistry.registerSigner()for newly attested signers. - Submit
TEEProverRegistry.deregisterSigner()for onchain signers whose instances are no longer reachable. - Submit
CertManager.revokeCert()for any certificate found to be revoked.
The registrar deliberately does not gate which PCR0 measurements are admitted. Registration is PCR0-agnostic so the next image’s signers can be pre-registered ahead of a hardfork. Whether a proof produced by a given signer is actually accepted is decided onchain by TEEVerifier, which compares against the active game implementation’s current TEE_IMAGE_HASH.
The registrar also does not build proposals, generate proof material for proposals or disputes, or challenge invalid state transitions. Those duties live with the proposer, the TEE provers, and the challenger.
Startup configuration
Section titled “Startup configuration”At startup, the registrar connects to:
- an L1 execution RPC for contract reads and transaction submission
- AWS APIs for ELBv2 target health and EC2 instance metadata
- a JSON-RPC endpoint on each discovered TEE prover instance
TEEProverRegistry- the
NitroValidatorthat the registry reports throughNITRO_VALIDATOR(), and theCertManagerthat validator points to
The operator supplies only the registry address. The registrar reads the validator and certificate manager addresses from the chain at startup and refuses to start if either is zero. Any onchain signer that is not present in its own instance set is treated as an orphan candidate, so a given TEEProverRegistry must be written by exactly one registrar.
Driver loop
Section titled “Driver loop”The service runs a single driver loop:
- Discover the current instance set through AWS.
- Probe
readyzon every target that is not draining, and resolve signer keys and attestations concurrently, bounded bymax_concurrency. - Start a registration task for each eligible signer, and cancel tasks whose signer is no longer eligible.
- Read the onchain signer set and deregister orphans, but only when discovery was conclusive.
- Sleep
poll_intervalseconds, or exit on cancellation.
step() runs once at startup before the first sleep. Cancellation is observed promptly between ticks and inside long-running transaction retries, so the service can shut down without leaving partial state behind.
Instance discovery
Section titled “Instance discovery”Discovery is driven exclusively by AWS ALB target group polling — DNS, SRV, and Kubernetes discovery are not supported.
Each discovery cycle:
- Calls
elasticloadbalancingv2.DescribeTargetHealth(target_group_arn). - Drops non-instance targets (IDs that do not start with
i-). - Deduplicates instance IDs that appear on more than one port.
- Calls
ec2.DescribeInstances(instance_ids)to read each instance’s private IP and launch time. - Builds JSON-RPC endpoint URLs shaped like
http://{private_ip}:{prover_port}and pairs each one with its ALB-reported health state.
The registrar does not trust the ALB health state to decide who may register. The load balancer’s /healthz check only passes once an instance is registered, so relying on it would block every new instance from ever bootstrapping. Instead, the registrar calls the prover’s own readyz method on every target that is not Draining, right before it resolves that target. A target that answers becomes Healthy. A target that fails becomes Unhealthy. Only Healthy instances can start a new registration. A slow probe does not hold up the others, because each probe is paired with its own resolution.
When an instance drops out of discovery or turns unhealthy, the registrar keeps protecting the signers it last saw there for instance_cache_ttl_cycles cycles. If any instance is still unresolved, it skips the whole orphan pass for that tick.
An AWS API error or missing EC2 data aborts the tick and skips orphan cleanup. An empty target group is different: it counts as a conclusive answer.
Per-instance processing
Section titled “Per-instance processing”For each discovered instance, the registrar:
- Calls
enclave_signerPublicKeyto fetch the per-enclave SEC1 public keys. A single instance can host multiple enclaves, and each enclave has its own signer key. - Derives the Ethereum signer address from each public key as the last 20 bytes of
keccak256(uncompressed_pubkey_xy). - Returns immediately when no signers were reported — that instance contributes nothing to the active set, and the call becomes a no-op.
- Decides whether the instance is currently registerable. Only an instance whose
readyzprobe passed proceeds. ADraininginstance still adds its known signer addresses to the active set, so a rotation does not deregister it at once, but it starts no new registrations. - Calls
enclave_signerAttestationonce with one nonce per signer. Each nonce is deterministic:keccak256over the domain stringbase-proof-tee-registrar:attestation-nonce:v1, the registry address, and the signer address. That binds each attestation to one signer and one registry, and the registrar rejects any plan whose nonce does not match. - Runs CRL checks once per batch when CRL checking is enabled. Each enclave signs with its own key, but AWS Nitro attestations are signed by the parent EC2 instance’s Nitro Hypervisor, whose signing key is endorsed by a per-instance AWS-issued certificate chain. Every enclave on a given instance therefore lives under the same parent chain, so one CRL check per instance is sufficient.
- Runs the registration pipeline for each signer address.
All reachable instances contribute to the active signer set, including Draining and Unhealthy ones. This prevents an instance that is rotating in or out from being deregistered prematurely.
Registration plan and hints
Section titled “Registration plan and hints”For each signer that is not yet onchain, the registrar first turns the raw attestation into a registration plan. The planner parses the COSE_Sign1 envelope strictly, keeping the raw protected header and payload so the signed bytes can be rebuilt exactly. The plan lists the certificate chain from the AWS root down to the leaf, with each certificate’s hash, parent hash, and revocation ID.
Before any transaction, the plan must pass these checks:
- The signer in the plan equals the signer the instance reported.
- The nonce equals the deterministic nonce for that signer and registry.
- PCR0 is a 48-byte value and is not the debug-mode measurement.
- The chain starts at the pinned Nitro root, whose hash is
0x311d96fcd5c5e0ccf72ef548e2ea7d4c0cd53ad7c4cc49e67471aed41d61f185. - Every entry before the last is a CA certificate, the last is the leaf, and each parent hash links to the one before it.
- The attestation timestamp is not in the future and is younger than
max_attestation_age.
The registrar then computes the P-384 hints. It walks the same affine point schedule the onchain verifier uses and records every modular inverse as a 48-byte big-endian value. There is one hint set per certificate signature, plus one for the attestation signature. This runs on a blocking thread, and a semaphore caps how many hint jobs run at once.
The default freshness window is 3300 seconds, kept under the onchain MAX_AGE of 3600 seconds so a plan that passes locally still lands before it ages out onchain.
Certificate caching
Section titled “Certificate caching”CertManager stores every certificate it has already verified. Before registering, the registrar walks the chain and, for each certificate not yet cached, sends one caching transaction: verifyCACertWithHints for a CA and verifyClientCertWithHints for the leaf. A per-certificate lock stops two signers that share a chain from caching the same certificate twice. Once the leaf is cached, later attestations under that leaf take the short path. Cache writes need no permission, because the contract verifies every certificate and hint itself. After each caching transaction the registrar reads the cache again. If a transaction reports an error but the expected entry is now present and usable, it counts as a success. After a restart or a partial failure, the registrar resumes from the first certificate that is still missing.
Each signature check gets its own transaction, which keeps every transaction under the EIP-7825 per-transaction gas limit. For a typical Nitro chain with three non-root CAs, the count depends on what is already cached:
| Already cached | Transactions |
|---|---|
| Nothing | 5: three CA writes, one leaf write, one registration |
| CA chain only | 2: one leaf write, one registration |
| CA chain and leaf | 1: registration only |
The final registerSigner call carries no certificate hints. NitroValidator accepts it only if the whole chain is already cached and usable. Because a cold registration takes several transactions, the registrar checks attestation freshness again before every certificate, revocation, and registration transaction. That way it stops before it sends material that has gone stale.
Registration transactions
Section titled “Registration transactions”For each unregistered signer, the registrar:
- Calls
TEEProverRegistry.isRegisteredSigner(signer)and skips the signer if it returns true. - Builds the plan and hints, and caches any missing certificates, as described above.
- ABI-encodes
registerSigner(attestationTbs, signature, hints)(selector0xb39dc09d). - Submits the transaction through the L1 transaction manager.
- Retries failed submissions according to the rules below.
- Increments the registration counter on a successful receipt.
The transaction retry rules are:
| Failure | Required behavior |
|---|---|
| Retryable error | Sleep tx_retry_delay, then retry, up to max_tx_retries total attempts. |
ExecutionReverted revert | Return the error. The next cycle fetches a fresh attestation and builds a new plan. |
| Insufficient funds, fee cap | Treat as non-retryable. Surface the error and stop attempting this signer for the current cycle. |
| Reverted receipt | Treat as a transaction failure even when submission succeeded. |
| Reported error after mining | Re-read isRegisteredSigner(signer). If true, treat the attempt as success. |
The post-error reconciliation matters because fee-bumping and nonce races can surface errors even when the underlying transaction has already been mined. Without the recheck, the registrar would redo hinting and send another transaction for a signer that is already registered.
Transaction submission is cancellation-aware: both the active send and the inter-attempt sleep abort cleanly on shutdown, so the next process starts from a clean nonce state without committing a partial transaction.
Orphan deregistration
Section titled “Orphan deregistration”After every instance has been processed, the registrar reconciles the onchain signer set against the active set:
- Skip cleanup if discovery failed during this tick, or if cancellation was requested.
- Skip cleanup if any instance is still unresolved.
- Build the protected set: active signers, signers kept from the last-known cache, and signers with a registration task still pending.
- Read the onchain set with
TEEProverRegistry.getRegisteredSigners(). - Compute
orphans = onchain_signers - protected_signers. - For each orphan, in order:
- Recheck
isRegisteredSigner(signer). Skip if it returns false. - ABI-encode
deregisterSigner(signer)and submit it through the transaction manager.
- Recheck
Deregistration clears the registered flag and the stored image hash. The unresolved-instance rule and the last-known-signer grace period replace the older majority-reachable guard, which is no longer in the source. Together they keep a short AWS or VPC outage from deregistering live signers. The per-orphan isRegisteredSigner recheck is a race guard: the set returned by getRegisteredSigners() is read once per cycle, and a different writer might have deregistered the signer between that read and this transaction. Skipping signers that are already gone avoids wasted gas on a no-op transaction.
The procedure assumes a single registrar per TEEProverRegistry. Two registrars sharing a registry would each treat the other’s signers as orphans.
Certificate revocation
Section titled “Certificate revocation”Revocation now lives on CertManager and is keyed by a certificate’s issuer and serial number rather than by its position in a chain. The registrar applies two layers, in order.
Layer 1: onchain revocation pre-check
Section titled “Layer 1: onchain revocation pre-check”For the pinned root and every certificate in the plan, the registrar asks CertManager whether that ID is revoked. Any hit stops registration for that signer. This check runs even for signers that are already registered, so a revoked chain is noticed as early as possible.
Layer 2: AWS CRL distribution points
Section titled “Layer 2: AWS CRL distribution points”This layer runs only when CRL checking is on. For certificates that pass Layer 1, the registrar:
- Parses each CRL distribution point from the chain.
- Accepts only URLs whose host ends in
.amazonaws.comand contains thenitro-enclavekeyword. Redirects are off and responses are capped at 10 MiB. - Fetches the CRL with a 30-second timeout.
- Looks up the certificate’s serial number.
- Submits
CertManager.revokeCert(certId)for any revoked certificate it finds. - Blocks registration for that signer.
A recorded revocation protects every later registration that shares the same chain, because Layer 1 catches it on the next pass without a network fetch.
A CRL fetch or parse failure fails open and is retried on a later cycle. A revocation confirmed onchain always fails closed.
Revocation and expiry only block new registrations. They do not remove signers that are already registered. If a certificate under an existing signer is revoked, deregister that signer yourself.
Pending registration lifecycle
Section titled “Pending registration lifecycle”Each signer has at most one registration task in flight, keyed by its address. A task runs through these stages:
flowchart TB Start([signer not registered]) --> Plan[Build and validate plan] Plan --> Revoke{Revoked or on CRL?} Revoke -->|yes| Stop([Skip signer]) Revoke -->|no| Hints[Compute P-384 hints] Hints --> Cache[Cache missing certificates] Cache --> Send[tx_manager.send registerSigner] Send -->|Ok| Done([Registered]) Send -->|Retryable| Send Send -->|Stale or reverted| Next([Retry next cycle])A stale attestation, a reverted transaction, or a cancelled task all end the attempt. The next cycle starts again from a fresh attestation, so a signer cannot get stuck.
Onchain interactions
Section titled “Onchain interactions”The registrar issues the following contract calls. TEEProverRegistry.isValidSigner() is intentionally never called by the registrar — that predicate is enforced by TEEVerifier at proof-submission time and includes an image-hash match that the registrar itself cannot satisfy.
| Contract | Method | Caller path |
|---|---|---|
TEEProverRegistry | registerSigner(attestationTbs, signature, hints) | Per-signer registration transaction. |
TEEProverRegistry | NITRO_VALIDATOR() | Startup, to find the validator. |
NitroValidator | certManager() | Startup, to find the certificate manager. |
CertManager | verifyCACertWithHints / verifyClientCertWithHints | Caching a certificate that is not yet verified. |
TEEProverRegistry | deregisterSigner(signer) | Per-orphan deregistration transaction. |
TEEProverRegistry | isRegisteredSigner(signer) | Pre-check, post-error reconciliation, orphan race guard. |
TEEProverRegistry | getRegisteredSigners() | Once per cycle for orphan computation. |
CertManager | revokeCert(certId) | When an AWS CRL lists a certificate. |
CertManager | revocation lookup by issuer and serial | Layer-1 onchain revocation pre-check. |
PCR0 enforcement happens onchain at proof submission, not at registration. The registrar registers any enclave whose Nitro attestation verifies, regardless of PCR0 value. That makes it possible to bring up the next image’s fleet and pre-register its signers before a hardfork; those signers cannot produce accepted proposals until the active game implementation’s TEE_IMAGE_HASH matches their registered image hash.
Service lifecycle
Section titled “Service lifecycle”At startup, the registrar:
- Parses CLI configuration and validates it.
- Initializes tracing and installs the
rustlsring crypto provider. - Installs a signal handler that triggers a cancellation token.
- Initializes Prometheus metrics, including L1 wallet balance monitoring.
- Builds the L1 provider, transaction manager, AWS SDK clients, and discovery client.
- Builds the registry client, then reads the
NitroValidatorandCertManageraddresses from the chain. - Starts the health server and marks readiness.
- Starts the driver loop.
The health endpoint reports ready as soon as wiring completes. Connectivity gating is intentionally omitted because the registrar is outbound-only.
Each driver tick:
- Discovers instances and probes
readyz. - Processes instances concurrently.
- Computes orphans, but only when no instance is unresolved.
- Submits deregistration transactions for any confirmed orphans.
Shutdown is driven by a cancellation token. The driver loop exits, in-flight per-instance futures are dropped, the readiness flag clears, the up metric is set to zero, and the health server is joined.
Operator inputs
Section titled “Operator inputs”A registrar needs:
| Input | Env variable | Default |
|---|---|---|
| L1 RPC endpoint | L1_RPC_URL | none |
TEEProverRegistry address | TEE_PROVER_REGISTRY_ADDRESS | none |
| ALB target group ARN | TARGET_GROUP_ARN | none |
| AWS region | AWS_REGION | none |
| Prover JSON-RPC port | PROVER_PORT | 8000 |
| Maximum attestation age | MAX_ATTESTATION_AGE_SECS | 3300 |
| Poll interval | POLL_INTERVAL_SECS | 30 |
| Prover JSON-RPC timeout | PROVER_TIMEOUT_SECS | 30 |
| Concurrent instances per cycle | MAX_CONCURRENCY | built-in default |
| Cycles to keep signers of an instance missing from discovery | INSTANCE_CACHE_TTL_CYCLES | built-in default |
| Transaction retries | MAX_TX_RETRIES | built-in default |
| First retry delay | TX_RETRY_DELAY_SECS | built-in default |
It also needs an L1 transaction signer (a local key, or a remote signer plus the expected address) and the transaction manager settings. Retry delays double on each attempt, up to 60 seconds.
Optional inputs are CRL_NITRO_VERIFIER_ADDRESS (turns CRL checks on, see the caution above), the health server bind address and port, logging, and Prometheus metrics settings. The Boundless, RISC Zero, and guest ELF inputs are gone.
Safety requirements
Section titled “Safety requirements”Any registrar implementation must preserve these safety properties:
- Never deregister live signers because of a transient AWS or VPC outage. Skip the orphan pass while any instance is unresolved, and honour the last-known-signer grace period.
- Keep the known signers of
Drainingand recently unavailable instances in the active set, so rotations do not race deregistration. - Recheck attestation freshness before each transaction in a multi-transaction cold registration.
- Request each attestation with the deterministic per-signer nonce and reject any plan whose nonce, signer, root, or chain links do not match.
- Reject debug-mode PCR0 values before any transaction.
- Reject attestations older than
max_attestation_age, keeping every submission inside the onchainMAX_AGEwindow. - Recheck
isRegisteredSignerafter a transaction error to absorb fee-bump and nonce-race false negatives. - Recheck
isRegisteredSignerfor every orphan candidate immediately before submitting a deregistration, so a concurrent writer or an earlier in-flight transaction cannot cause a redundant deregistration. - Run the onchain revocation pre-check before fetching network CRLs, so a certificate revoked once stays revoked.
- Restrict CRL fetches to allowlisted hosts and bound the response size to defeat SSRF and resource-exhaustion attacks.
- Treat unavailable AWS APIs, unreachable prover endpoints, and transient RPC errors as retryable conditions for the next tick — not as deregistration or failure signals.