Skip to content
BaseHub by Will Binns Updated

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.

A conforming registrar performs the following work each cycle:

  1. Discover the current set of TEE prover instances behind the production load balancer.
  2. Fetch per-enclave signer public keys and Nitro attestation documents from each instance.
  3. Check the attestation certificate chain against the onchain revocation set and, when enabled, against AWS-published CRLs.
  4. Compute P-384 hints for every enclave that is not yet registered, and cache any certificate in the chain that CertManager has not verified yet.
  5. Submit TEEProverRegistry.registerSigner() for newly attested signers.
  6. Submit TEEProverRegistry.deregisterSigner() for onchain signers whose instances are no longer reachable.
  7. 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.

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 NitroValidator that the registry reports through NITRO_VALIDATOR(), and the CertManager that 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.

The service runs a single driver loop:

  1. Discover the current instance set through AWS.
  2. Probe readyz on every target that is not draining, and resolve signer keys and attestations concurrently, bounded by max_concurrency.
  3. Start a registration task for each eligible signer, and cancel tasks whose signer is no longer eligible.
  4. Read the onchain signer set and deregister orphans, but only when discovery was conclusive.
  5. Sleep poll_interval seconds, 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.

Discovery is driven exclusively by AWS ALB target group polling — DNS, SRV, and Kubernetes discovery are not supported.

Each discovery cycle:

  1. Calls elasticloadbalancingv2.DescribeTargetHealth(target_group_arn).
  2. Drops non-instance targets (IDs that do not start with i-).
  3. Deduplicates instance IDs that appear on more than one port.
  4. Calls ec2.DescribeInstances(instance_ids) to read each instance’s private IP and launch time.
  5. 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.

For each discovered instance, the registrar:

  1. Calls enclave_signerPublicKey to fetch the per-enclave SEC1 public keys. A single instance can host multiple enclaves, and each enclave has its own signer key.
  2. Derives the Ethereum signer address from each public key as the last 20 bytes of keccak256(uncompressed_pubkey_xy).
  3. Returns immediately when no signers were reported — that instance contributes nothing to the active set, and the call becomes a no-op.
  4. Decides whether the instance is currently registerable. Only an instance whose readyz probe passed proceeds. A Draining instance 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.
  5. Calls enclave_signerAttestation once with one nonce per signer. Each nonce is deterministic: keccak256 over the domain string base-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.
  6. 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.
  7. 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.

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.

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 cachedTransactions
Nothing5: three CA writes, one leaf write, one registration
CA chain only2: one leaf write, one registration
CA chain and leaf1: 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.

For each unregistered signer, the registrar:

  1. Calls TEEProverRegistry.isRegisteredSigner(signer) and skips the signer if it returns true.
  2. Builds the plan and hints, and caches any missing certificates, as described above.
  3. ABI-encodes registerSigner(attestationTbs, signature, hints) (selector 0xb39dc09d).
  4. Submits the transaction through the L1 transaction manager.
  5. Retries failed submissions according to the rules below.
  6. Increments the registration counter on a successful receipt.

The transaction retry rules are:

FailureRequired behavior
Retryable errorSleep tx_retry_delay, then retry, up to max_tx_retries total attempts.
ExecutionReverted revertReturn the error. The next cycle fetches a fresh attestation and builds a new plan.
Insufficient funds, fee capTreat as non-retryable. Surface the error and stop attempting this signer for the current cycle.
Reverted receiptTreat as a transaction failure even when submission succeeded.
Reported error after miningRe-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.

After every instance has been processed, the registrar reconciles the onchain signer set against the active set:

  1. Skip cleanup if discovery failed during this tick, or if cancellation was requested.
  2. Skip cleanup if any instance is still unresolved.
  3. Build the protected set: active signers, signers kept from the last-known cache, and signers with a registration task still pending.
  4. Read the onchain set with TEEProverRegistry.getRegisteredSigners().
  5. Compute orphans = onchain_signers - protected_signers.
  6. For each orphan, in order:
    1. Recheck isRegisteredSigner(signer). Skip if it returns false.
    2. ABI-encode deregisterSigner(signer) and submit it through the transaction manager.

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.

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.

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.

This layer runs only when CRL checking is on. For certificates that pass Layer 1, the registrar:

  1. Parses each CRL distribution point from the chain.
  2. Accepts only URLs whose host ends in .amazonaws.com and contains the nitro-enclave keyword. Redirects are off and responses are capped at 10 MiB.
  3. Fetches the CRL with a 30-second timeout.
  4. Looks up the certificate’s serial number.
  5. Submits CertManager.revokeCert(certId) for any revoked certificate it finds.
  6. 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.

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.

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.

ContractMethodCaller path
TEEProverRegistryregisterSigner(attestationTbs, signature, hints)Per-signer registration transaction.
TEEProverRegistryNITRO_VALIDATOR()Startup, to find the validator.
NitroValidatorcertManager()Startup, to find the certificate manager.
CertManagerverifyCACertWithHints / verifyClientCertWithHintsCaching a certificate that is not yet verified.
TEEProverRegistryderegisterSigner(signer)Per-orphan deregistration transaction.
TEEProverRegistryisRegisteredSigner(signer)Pre-check, post-error reconciliation, orphan race guard.
TEEProverRegistrygetRegisteredSigners()Once per cycle for orphan computation.
CertManagerrevokeCert(certId)When an AWS CRL lists a certificate.
CertManagerrevocation lookup by issuer and serialLayer-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.

At startup, the registrar:

  1. Parses CLI configuration and validates it.
  2. Initializes tracing and installs the rustls ring crypto provider.
  3. Installs a signal handler that triggers a cancellation token.
  4. Initializes Prometheus metrics, including L1 wallet balance monitoring.
  5. Builds the L1 provider, transaction manager, AWS SDK clients, and discovery client.
  6. Builds the registry client, then reads the NitroValidator and CertManager addresses from the chain.
  7. Starts the health server and marks readiness.
  8. 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:

  1. Discovers instances and probes readyz.
  2. Processes instances concurrently.
  3. Computes orphans, but only when no instance is unresolved.
  4. 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.

A registrar needs:

InputEnv variableDefault
L1 RPC endpointL1_RPC_URLnone
TEEProverRegistry addressTEE_PROVER_REGISTRY_ADDRESSnone
ALB target group ARNTARGET_GROUP_ARNnone
AWS regionAWS_REGIONnone
Prover JSON-RPC portPROVER_PORT8000
Maximum attestation ageMAX_ATTESTATION_AGE_SECS3300
Poll intervalPOLL_INTERVAL_SECS30
Prover JSON-RPC timeoutPROVER_TIMEOUT_SECS30
Concurrent instances per cycleMAX_CONCURRENCYbuilt-in default
Cycles to keep signers of an instance missing from discoveryINSTANCE_CACHE_TTL_CYCLESbuilt-in default
Transaction retriesMAX_TX_RETRIESbuilt-in default
First retry delayTX_RETRY_DELAY_SECSbuilt-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.

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 Draining and 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 onchain MAX_AGE window.
  • Recheck isRegisteredSigner after a transaction error to absorb fee-bump and nonce-race false negatives.
  • Recheck isRegisteredSigner for 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.