diff --git a/docs/AGENTS.md b/docs/AGENTS.md index fc43973d0..27c7c235a 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -55,7 +55,7 @@ npx skills add base/base-skills |Specifications/Specifications/Base Protocol/Consensus:specifications/base-protocol/consensus/specification,specifications/base-protocol/consensus/derivation,specifications/base-protocol/consensus/p2p,specifications/base-protocol/consensus/rpc |Specifications/Specifications/Base Protocol/Execution:specifications/base-protocol/execution/l2-execution-engine,specifications/base-protocol/execution/precompiles,specifications/base-protocol/execution/predeploys,specifications/base-protocol/execution/preinstalls |Specifications/Specifications/Base Protocol/Bridging:specifications/base-protocol/bridging/standard-bridges,specifications/base-protocol/bridging/deposits,specifications/base-protocol/bridging/withdrawals,specifications/base-protocol/bridging/cross-domain-messengers,specifications/base-protocol/bridging/base-solana-bridge -|Specifications/Specifications/Base Protocol/Proofs:specifications/base-protocol/proofs/overview,specifications/base-protocol/proofs/challenger,specifications/base-protocol/proofs/proposer,specifications/base-protocol/proofs/registrar,specifications/base-protocol/proofs/tee-prover,specifications/base-protocol/proofs/zk-prover,specifications/base-protocol/proofs/proof-contracts +|Specifications/Specifications/Base Protocol/Proofs:specifications/base-protocol/proofs/overview,specifications/base-protocol/proofs/challenger,specifications/base-protocol/proofs/proposer,specifications/base-protocol/proofs/hinted-tee-registration,specifications/base-protocol/proofs/registrar,specifications/base-protocol/proofs/tee-prover,specifications/base-protocol/proofs/zk-prover,specifications/base-protocol/proofs/proof-contracts |Specifications/Specifications/B20:specifications/b20/specification-overview,specifications/b20/reference/constants-addresses,specifications/b20/reference/errors-events,specifications/b20/reference/invariants-tests,specifications/b20/changelog |Specifications/Specifications/B20/Interfaces:specifications/b20/reference/interfaces/i-activation-registry/index,specifications/b20/reference/interfaces/ib20/index,specifications/b20/reference/interfaces/ib20-asset/index,specifications/b20/reference/interfaces/ib20-factory/index,specifications/b20/reference/interfaces/ib20-stablecoin/index,specifications/b20/reference/interfaces/i-policy-registry/index |Specifications/Specifications/Transactions:specifications/transactions/transaction-ordering,specifications/transactions/transaction-finality,specifications/transactions/network-fees,specifications/transactions/throughput-and-limits,specifications/transactions/troubleshooting-transactions diff --git a/docs/docs.json b/docs/docs.json index ed72e6c4d..3c08131a5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -216,6 +216,7 @@ "specifications/base-protocol/proofs/overview", "specifications/base-protocol/proofs/challenger", "specifications/base-protocol/proofs/proposer", + "specifications/base-protocol/proofs/hinted-tee-registration", "specifications/base-protocol/proofs/registrar", "specifications/base-protocol/proofs/tee-prover", "specifications/base-protocol/proofs/zk-prover", diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 11ae273fd..d8cb13c7a 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -264,7 +264,9 @@ const client = createPublicClient({ chain: base, transport: http() }) - [Proposer](https://docs.base.org/specifications/base-protocol/proofs/proposer): Specification of the proposer, an offchain service that turns canonical L2 checkpoint ranges into AggregateVerifier games on L1. -- [Registrar](https://docs.base.org/specifications/base-protocol/proofs/registrar): Specification of the registrar, an offchain service that maintains the onchain registry of accepted TEE signer identities via attestation ZK proofs. +- [Hinted TEE Registration](https://docs.base.org/specifications/base-protocol/proofs/hinted-tee-registration): Migration from external ZK proving to checked P-384 hints for registering new AWS Nitro Enclave signers. + +- [Registrar](https://docs.base.org/specifications/base-protocol/proofs/registrar): Specification of the registrar and the hinted P-384 flow used to register AWS Nitro Enclave signer identities on L1. - [TEE Prover](https://docs.base.org/specifications/base-protocol/proofs/tee-prover): Specification of the TEE prover, an offchain service that re-executes L2 block ranges inside AWS Nitro Enclaves to produce signed proof material for AggregateVerifier games. diff --git a/docs/llms.txt b/docs/llms.txt index c93ad4e34..ffa1f6cc3 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -198,7 +198,9 @@ - [Proposer](https://docs.base.org/specifications/base-protocol/proofs/proposer): Specification of the proposer, an offchain service that turns canonical L2 checkpoint ranges into AggregateVerifier games on L1. -- [Registrar](https://docs.base.org/specifications/base-protocol/proofs/registrar): Specification of the registrar, an offchain service that maintains the onchain registry of accepted TEE signer identities via attestation ZK proofs. +- [Hinted TEE Registration](https://docs.base.org/specifications/base-protocol/proofs/hinted-tee-registration): Migration from external ZK proving to checked P-384 hints for registering new AWS Nitro Enclave signers. + +- [Registrar](https://docs.base.org/specifications/base-protocol/proofs/registrar): Specification of the registrar and the hinted P-384 flow used to register AWS Nitro Enclave signer identities on L1. - [TEE Prover](https://docs.base.org/specifications/base-protocol/proofs/tee-prover): Specification of the TEE prover, an offchain service that re-executes L2 block ranges inside AWS Nitro Enclaves to produce signed proof material for AggregateVerifier games. diff --git a/docs/specifications/base-protocol/proofs/hinted-tee-registration.mdx b/docs/specifications/base-protocol/proofs/hinted-tee-registration.mdx new file mode 100644 index 000000000..2960ac9b1 --- /dev/null +++ b/docs/specifications/base-protocol/proofs/hinted-tee-registration.mdx @@ -0,0 +1,77 @@ +--- +title: "Hinted TEE Registration" +description: "Migration from external ZK proving to checked P-384 hints for registering new AWS Nitro Enclave signers." +--- + +Base is replacing the RISC Zero and Boundless flow used to register new TEE signers with direct +AWS Nitro attestation verification using checked P-384 hints. + +The change makes registration faster and removes the dependency on an external ZK proving service. +It does not change how enclaves produce TEE state proofs or how those proofs are verified. + +## Previous Flow + +The registrar previously sent each Nitro attestation to an external proving service. That service +produced a RISC Zero proof showing that the attestation and its AWS certificate chain were valid. +The registrar then submitted the proof to `TEEProverRegistry` on L1. + +This added an external availability dependency and could delay new signer registration while the ZK +proof was generated. + +## New Flow + +The registrar now: + +1. Fetches a fresh AWS Nitro attestation for the new enclave signer. +2. Generates P-384 modular inverse hints locally. +3. Verifies and caches any missing AWS certificates through `CertManager`. +4. Submits the attestation, its signature, and the final hint stream to `TEEProverRegistry`. +5. Records the signer and its attested PCR0 after all onchain checks pass. + +`TEEProverRegistry` permits only its owner or manager to perform the final registration. Certificate +cache writes are permissionless because `CertManager` verifies the certificate, its parent chain, +and every supplied hint before storing it. + +## Checked Hints + +P-384 verification requires many modular inversions. Computing those inversions in the EVM is +expensive, but checking a proposed inverse is comparatively cheap. The registrar computes the +values offchain and supplies them as hints. + +Every hint is checked onchain before use. Incorrect, missing, or surplus hints revert, so the hint +generator is required for liveness but is not trusted for correctness. A faulty or malicious hint +cannot make an invalid certificate or attestation pass. + +## Certificate Caching + +The AWS Nitro root is pinned in `CertManager`. Non-root CA certificates and the leaf certificate are +verified and cached in parent-first order. Later registrations can reuse cached certificates while +they remain valid and unrevoked. + +For a typical Nitro certificate chain: + +| Cache state | Registration transactions | +| --- | ---: | +| Empty | 5 | +| CA chain cached, new leaf | 2 | +| CA chain and leaf cached | 1 | + +The registrar reads onchain cache state before each write, allowing it to resume after a partial +registration or process restart without repeating completed certificate transactions. + +## Unchanged Behavior + +This migration does not change: + +- signer key generation inside the Nitro Enclave; +- the attestation fields produced by AWS Nitro; +- PCR0-based enclave image selection; +- TEE proposal or dispute proof formats; +- `TEEVerifier` or `AggregateVerifier` behavior; or +- SP1 state-proof verification. + +The existing `TEEProverRegistry` proxy is upgraded without changing its storage layout. Existing +owners, managers, proposers, registered signers, and signer image hashes are preserved. + +For the full registration lifecycle, validation rules, revocation handling, and operator +requirements, see the [Registrar specification](/specifications/base-protocol/proofs/registrar). diff --git a/docs/specifications/base-protocol/proofs/registrar.mdx b/docs/specifications/base-protocol/proofs/registrar.mdx index 48668a06a..560e92347 100644 --- a/docs/specifications/base-protocol/proofs/registrar.mdx +++ b/docs/specifications/base-protocol/proofs/registrar.mdx @@ -1,412 +1,306 @@ --- title: "Registrar" -description: "Specification of the registrar, an offchain service that maintains the onchain registry of accepted TEE signer identities via attestation ZK proofs." +description: "Specification of the registrar and the hinted P-384 flow used to register AWS Nitro Enclave signer identities on L1." --- -The registrar is an offchain service that maintains the onchain registry of accepted TEE signer -identities. It discovers running TEE prover instances, fetches AWS Nitro Enclave attestation -documents from each enclave, generates a ZK proof that the attestation is well-formed, and submits -the resulting signer registration to [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) -on L1. It also deregisters signers whose backing instances are no longer reachable, and revokes -intermediate certificates that AWS has withdrawn. +The registrar is an offchain service that maintains the signer set in +[`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol). +It discovers Base TEE prover instances, obtains an AWS Nitro attestation for each enclave signer, +generates P-384 verification hints locally, caches the attestation certificate chain on L1, and +submits the final signer registration. -A registrar is operated by Base. The proof system trusts only signers that this registrar has -registered, so registrar correctness is a prerequisite for accepting TEE proofs onchain. Its output -is still self-validating: the attestation ZK proof, the enclave PCR0 measurement, and the signer -public key are all checked by `TEEProverRegistry` and [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) -before the signer becomes valid. +This flow replaces the previous RISC Zero attestation proof and external Boundless proving path. +There is no runtime backend selector or fallback to ZK verification. The TEE proof format, +`TEEVerifier`, `AggregateVerifier`, `TEE_IMAGE_HASH`, and SP1 state-proof behavior are unchanged. +Registration requires the hinted-compatible registrar and prover-host signer API. ## Responsibilities A conforming registrar performs the following work: -1. Discover the current set of TEE prover instances behind the production load balancer. -2. Fetch the per-enclave signer public keys and Nitro attestation documents from each instance. -3. Optionally check the attestation certificate chain against AWS-published CRLs and against the - onchain durable revocation set. -4. Generate a ZK proof of attestation correctness for every enclave that is not yet registered. -5. Submit `TEEProverRegistry.registerSigner()` for newly attested signers. -6. Submit `TEEProverRegistry.deregisterSigner()` for onchain signers whose instances are gone. -7. Submit `NitroEnclaveVerifier.revokeCert()` for intermediate certificates discovered to be - revoked. -8. Recover in-flight proof requests across process restarts without re-spending proving work. - -The registrar does not gate which PCR0 measurements are accepted. Registration is PCR0-agnostic so -that the next image's signers can be pre-registered ahead of a hardfork. Acceptance of proofs -produced by a given signer is enforced onchain by [`TEEVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEVerifier.sol) -against the current `TEE_IMAGE_HASH` of the active game implementation. - -The registrar also does not create proposals, generate proof material for proposals or disputes, -or dispute invalid state transitions. Those responsibilities belong to the proposer, the TEE -provers, and the challenger. - -## 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 -- a proving backend (Boundless marketplace or a self-hosted RISC Zero prover) -- `TEEProverRegistry` -- an optional `NitroEnclaveVerifier`, required only when CRL checking is enabled - -The registrar reads no contract configuration at startup beyond the registry and verifier -addresses provided by the operator. It treats every onchain signer it has not seen in its own -instance set as an orphan candidate, so a single registrar must be the sole writer for a given -registry. +1. Discover TEE prover instances behind the configured AWS load balancer. +2. Fetch each enclave's secp256k1 public key and a fresh Nitro attestation bound to that signer. +3. Parse the COSE and X.509 data without changing the signed encodings. +4. Validate the expected signer, nonce, PCR0, trust anchor, certificate order, and freshness locally. +5. Generate the checked P-384 inverse hints required by the onchain verifier. +6. Reuse valid cached certificates and cache missing certificates in parent-first order. +7. Submit the final attestation to `TEEProverRegistry.registerSigner()`. +8. Optionally monitor AWS certificate revocation lists and persist confirmed revocations on L1. +9. Deregister signers whose backing prover instances are no longer active. + +The registrar does not select the accepted enclave image. Registration records the attested PCR0, +but [`TEEVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEVerifier.sol) +accepts a signer only when its recorded image hash matches the active `TEE_IMAGE_HASH`. This allows +the next image's signers to be registered before an image rotation without allowing them to produce +accepted proofs early. + +## Architecture + +```mermaid TEE Signer Registration lines wrap expandable +flowchart LR + registrar[Registrar] + prover[TEE prover instance] + registry[TEEProverRegistry] + validator[NitroValidator] + certs[CertManager] + p384[P384Verifier] + + prover -- "signer key + attestation" --> registrar + registrar -- "cache certificate + hints" --> certs + registrar -- "attestation TBS + signature + hints" --> registry + registry --> validator + validator --> certs + certs --> p384 + validator --> p384 +``` -## Driver Loop +The onchain validation stack contains three contracts: -The registrar runs a single driver loop: - -1. Discover the current instance set. -2. Process every instance concurrently, bounded by `max_concurrency`. -3. Read the onchain signer set. -4. Deregister orphan signers. -5. Sleep `poll_interval` seconds, or exit on cancellation. - -The loop runs `step()` once on startup before sleeping. Cancellation is observed promptly between -ticks and inside long-running tx retries so the service can shut down without leaving partial -state. - -## Instance Discovery - -The registrar uses AWS ALB target group polling. DNS, SRV, and Kubernetes discovery are not -supported. - -Each discovery cycle: - -1. Calls `elasticloadbalancingv2.DescribeTargetHealth(target_group_arn)`. -2. Filters out non-instance targets (target 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 of the form `http://{private_ip}:{prover_port}` and pairs each - with its ALB-reported health state. - -Health states map as follows: - -| AWS state | Internal state | `should_register()` | -| ------------ | -------------- | ------------------- | -| `initial` | `Initial` | true | -| `healthy` | `Healthy` | true | -| `draining` | `Draining` | false | -| anything else| `Unhealthy` | false | - -`Unhealthy` instances within `unhealthy_registration_window` seconds of `launch_time` are still -allowed to register. This is a warm-up grace period: it lets a new instance whose JSON-RPC -endpoint is briefly slow finish enclave attestation and registration before the next ALB health -check would deregister it. The window must be smaller than the Boundless proving timeout so that -a started proof can complete before the instance becomes ineligible. - -Discovery failures abort that tick and skip orphan cleanup. They do not deregister live signers. - -## Per-Instance Processing - -For each discovered instance, the registrar: - -1. Calls `enclave_signerPublicKey` to fetch the per-enclave SEC1 public keys. Each 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 if no signers were reported. The address set still contributes nothing for - this instance and the call is a no-op. -4. Decides whether the instance is currently registerable: - - `Initial` and `Healthy` instances proceed. - - `Unhealthy` instances within the warm-up window proceed. - - All other instances contribute their addresses to the active set but do not generate new - proofs or transactions. -5. Generates a single 32-byte random nonce and calls `enclave_signerAttestation` once with that - nonce. The nonce binds every per-enclave attestation in the returned batch to the same - freshness commitment. -6. Performs CRL checks once per batch when CRL checking is enabled. Each enclave has its own - signing 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 the same instance therefore produces an attestation under the same parent - chain, so a single CRL check per instance is sufficient. -7. For each signer address, runs the registration pipeline. - -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. - -## Attestation Proof Generation - -The registrar produces proof material for every signer not yet onchain by calling an -`AttestationProofProvider`. The provider returns: - -```text Attestation Proof Provider Output -output // ABI-encoded VerifierJournal (PCRs, public key, timestamp, cert hashes) -proofBytes // Groth16 seal -``` +| Contract | Role | +| --- | --- | +| `P384Verifier` | Verifies P-384 signatures while checking caller-supplied modular inverse hints. | +| `CertManager` | Pins the AWS Nitro root, verifies and caches non-root certificates, and enforces certificate expiry and revocation. | +| `NitroValidator` | Parses the signed Nitro attestation, re-walks the cached certificate chain, and verifies the final COSE signature. | + +`TEEProverRegistry` holds an immutable `NitroValidator` reference. The registrar discovers +`CertManager` through `TEEProverRegistry.NITRO_VALIDATOR()` and `NitroValidator.certManager()`; +operators do not configure those addresses separately. -`output` is the `VerifierJournal` consumed by `NitroEnclaveVerifier.verify()` during -`registerSigner()`. `proofBytes` is the Groth16 SNARK that proves the journal corresponds to a -valid Nitro attestation document. +## Driver Loop -The registrar supports two backends: +The registrar runs one polling loop: -| Backend | Description | -| ----------- | ------------------------------------------------------------------------------------------------------------ | -| `boundless` | Submits the proving job to the Boundless marketplace using a dedicated wallet. | -| `direct` | Loads the guest ELF locally and proves via `risc0_zkvm::default_prover()`, routing to Bonsai or a local prover according to RISC Zero environment variables. | +1. Query AWS ALB and EC2 for the current prover instances. +2. Probe `/readyz` on every non-draining target. The load balancer's `/healthz` is registration-gated, + so it cannot be used to bootstrap registration. +3. Resolve signer public keys and attestations concurrently, bounded by `max_concurrency`. +4. Start one registration task per eligible signer and cancel tasks whose signer is no longer eligible. +5. Read the onchain signer set and deregister unprotected signers when discovery was conclusive. +6. Sleep for `poll_interval`, or stop on cancellation. -Both backends are valid production paths. `boundless` is the primary production backend. -`direct` is also used for local development and tests, but it is suitable for production fallback -when an operator needs to bypass the marketplace, for example during a Boundless incident or for -private-deployment scenarios. +Only instances that pass `/readyz` are eligible for new registrations. Draining instances still +contribute their known signer addresses to the active set so a rotation does not immediately +deregister them. -For Boundless, the registrar submits a `RequestParams` containing the program URL, the attestation -input, the expected `image_id`, and a `prefix_match(image_id)` requirement so a fulfilled request -cannot be replayed against a different program. Onchain Boundless submissions are serialized -behind a mutex to avoid wallet nonce races. +When an instance disappears or becomes unhealthy, the registrar preserves its last-known signers +for `instance_cache_ttl_cycles`. It skips the entire orphan pass while any instance remains +unresolved. Pending registration tasks are also protected from orphan cleanup. -### Restart Recovery +Discovery supports AWS ELBv2 target groups whose targets are EC2 instances. Targets whose IDs do +not start with `i-` are ignored. AWS API errors and missing EC2 data abort the tick and skip orphan +cleanup, but an empty target group is a conclusive result. The last-known-signer cache is in memory +and starts empty after a process restart. Operators must therefore configure and monitor the target +group carefully: a cold registrar pointed at an empty target group has no cached signers to protect. -The registrar process is itself ephemeral. Across restarts, it must not re-spend proving work and -must not submit stale proofs. Boundless `RequestId` slots are derived deterministically: +## Attestation Challenge -```text Request Index Derivation -request_index(signer, attempt) = u32::from_be_bytes(keccak256(signer || attempt)[..4]) -``` +Each signer receives a deterministic 32-byte nonce: -For each signer, the registrar probes `max_recovery_attempts` consecutive deterministic slots -before submitting a fresh request. The action depends on the slot status: +```text Attestation Nonce +keccak256( + "base-proof-tee-registrar:attestation-nonce:v1" || + teeProverRegistryAddress || + signerAddress +) +``` -| Slot status | Registrar action | -| ------------- | --------------------------------------------------------------------------------- | -| `Unknown` | Record the first such slot as the candidate fresh-submission slot; keep scanning. | -| `Locked` | Resume `wait_for_request_fulfillment` and use the resulting receipt. | -| `Fulfilled` | Fetch the receipt and check journal freshness before accepting it. | -| `Expired` | Skip the slot permanently; continue scanning. | +The registrar requests one attestation per signer with that nonce. It rejects the response unless: -A `RequestIsNotLocked` revert encountered mid-scan is treated as in-flight and short-circuits to -waiting on that slot. +- the attested `public_key` derives to the expected signer address; +- the attested nonce exactly matches the deterministic challenge; +- PCR0 is present, is 48 bytes, and is not the all-zero debug-mode measurement; +- the certificate chain starts at the pinned AWS Nitro root; +- the certificate chain is ordered parent-first and ends in one leaf certificate; and +- the attestation remains within the configured local freshness window. -If a recovered receipt's attestation timestamp is older than `max_attestation_age`, the registrar -discards it and submits a fresh request in the candidate slot. The default freshness window is -3300 seconds, kept strictly under the onchain `MAX_AGE` of 3600 seconds so a recovered proof can -still be submitted before it ages out onchain. +The deterministic challenge binds the attestation to one registry and signer without requiring +registrar state across restarts. `TEEProverRegistry` independently enforces timestamp freshness, +but nonce matching is a registrar policy check because `NitroValidator` only exposes the signed +nonce to its caller. -After an `ExecutionReverted` from `registerSigner()`, the signer is added to a per-process -`recovery_blocked` set. The next cycle skips the recovery scan for that signer and submits a fresh -request, so a known-bad recovered proof is never tried twice. The set is cleared on restart, which -gives one fresh attempt per process even for previously blocked signers. +## Registration Plan -## Registration Transactions +The registrar preserves the exact protected-header and payload encodings from the input +`COSE_Sign1` document when constructing the signed `Sig_structure`. Re-encoding signed CBOR would +change the message and invalidate the AWS signature. -For each unregistered signer, the registrar: +For parity with the pinned `NitroValidator`, the registrar accepts an optional compact `0xD2` tag +followed by the compact `0x84` `COSE_Sign1` array, the ES384 protected header `0x44a1013822`, a +96-byte P-384 signature, and no trailing COSE or TBS data. The payload, `pcrs`, and `cabundle` +containers may use definite or indefinite lengths. Unknown payload keys are skipped, but recognized +keys must be unique. -1. Calls `TEEProverRegistry.isRegisteredSigner(signer)`. If true, the signer is skipped. -2. Generates or recovers proof material as described above. -3. ABI-encodes `registerSigner(output, proofBytes)`. -4. Submits the transaction through the L1 transaction manager. -5. Retries failed submissions according to the rules below. -6. On a successful receipt, increments the registration counter. +The parsed plan contains: -The transaction retry rules are: +- the signer derived from the 65-byte `0x04 || x || y` secp256k1 public key; +- PCR0, timestamp, and nonce; +- the pinned root certificate; +- non-root CA certificates followed by the leaf certificate; +- the exact attestation to-be-signed bytes and 96-byte P-384 signature; and +- cache and revocation identifiers matching `CertManager`. -| Failure | Required behavior | -| ----------------------------- | ------------------------------------------------------------------------------------------------------- | -| Retryable error | Sleep `tx_retry_delay`, then retry, up to `max_tx_retries` total attempts. | -| `ExecutionReverted` revert | Block recovery for this signer so the next cycle generates a fresh proof, then return the error. | -| 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 pinned root cache key is `keccak256(root DER)`. Every non-root cache key is +`keccak256(TBSCertificate DER)`, excluding the malleable outer ECDSA signature. Non-root revocation +uses `keccak256(issuerHash || serialHash)`. `issuerHash` hashes the issuer Name content octets, +excluding its DER tag and length. `serialHash` hashes the serial INTEGER content octets, including a +leading `0x00` used for DER sign extension. These identities remain stable across equivalent outer +certificate encodings. -The post-error reconciliation is required because fee-bumping and nonce races can return errors -even when the underlying transaction has already been mined. Without the recheck, the registrar -would burn proving work generating a fresh proof for an already-registered signer. +## P-384 Hints -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. +AWS signs Nitro certificates and attestations with ECDSA over P-384. P-384 verification requires +many modular inversions, which are expensive to compute in the EVM. The registrar computes each +inverse offchain and supplies it as a hint. -## Orphan Deregistration +For every division by `b` modulo a prime `m`, the contract checks: -After processing every instance, the registrar reconciles the onchain signer set against the -active set: +```text Inverse Check +b * hint == 1 (mod m) +``` -1. If discovery failed for this tick, skip cleanup. -2. If cancellation was requested, skip cleanup. -3. Compare the number of reachable instances against the total discovered instances. If - `reachable_instances * 2 <= total_instances`, skip cleanup. -4. Read the onchain set with `TEEProverRegistry.getRegisteredSigners()`. -5. Compute `orphans = onchain_signers \ active_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. +The hint is used only after this equality holds. Since an invertible value has one inverse modulo a +prime, a passing hint is the same value the contract would have computed. Incorrect, truncated, or +surplus hints revert. Hints affect liveness, not correctness: a faulty generator can prevent a +registration but cannot make an invalid signature pass. -The majority-reachable guard prevents a transient AWS or VPC outage from deregistering most of -the prover fleet at once. The per-orphan `isRegisteredSigner` recheck is a race guard: the set -returned by `getRegisteredSigners()` is read once per cycle, and another writer could have -deregistered a signer between that read and this transaction. Skipping already-deregistered -addresses avoids wasted gas on a no-op transaction. +Each hint stream is a concatenation of 48-byte big-endian inverses in the verifier's deterministic +consumption order. The registrar generates one stream for every non-root certificate signature and +one for the final attestation signature. Production hint generation is native Rust and does not +invoke Node, Go, or an external proving service. -This procedure assumes a single registrar per `TEEProverRegistry`. Two registrars sharing a -registry would each treat the other's signers as orphans. +## Certificate Cache -## Certificate Revocation +`CertManager` stores the pinned AWS Nitro root at deployment. The registrar processes every +remaining certificate in parent-first order: -When the operator enables CRL checking, the registrar enforces revocation using two layers in -order. Both are required to make CRL handling safe. +1. Read the candidate's cached metadata and revocation state. +2. If cached, require the expected CA or leaf role, an unexpired validity period, the original + parent binding, and a complete unrevoked path to the pinned root. +3. If not cached, call `verifyCACertWithHints()` or `verifyClientCertWithHints()` with the DER + certificate, parent cache key, and signature hints. +4. Re-read state after the transaction. A transaction error is treated as success if the expected + usable cache entry is now present. -### Layer 1: Onchain Durable Revocation Pre-Check +Cache writes are permissionless because all certificate data and hints are verified onchain. +Per-certificate locks prevent concurrent signer tasks from submitting duplicate cache transactions +for a shared chain. On restart or after a partial failure, the registrar reads the cache again and +continues from the first missing certificate. -For each intermediate certificate in the attestation chain, the registrar reads -`NitroEnclaveVerifier.revokedCerts(certPathDigest)`. Any hit blocks registration for that batch -and skips Layer 2 entirely. +For a typical Nitro chain with three non-root CAs, transaction counts are: -This layer protects against a known attack against the cached-cert path: an intermediate that was -once revoked onchain could be reintroduced through a later `_cacheNewCert` write if its CRL entry -is later pruned by AWS. Reading the durable mapping first ensures a revoked cert cannot be -silently rehabilitated. +| Cache state | Transactions | +| --- | ---: | +| Empty | 5: three CA cache writes, one leaf cache write, one registration | +| CA chain cached, new leaf | 2: one leaf cache write, one registration | +| CA chain and leaf cached | 1: registration only | -RPC errors against `revokedCerts` fail open and fall through to Layer 2, but are counted as -revocation check errors. `RegistrationDriver::new` requires a `NitroEnclaveVerifier` client when -CRL checking is enabled and rejects misconfiguration at startup. +Each signature verification is split into its own transaction so it remains below the EIP-7825 +per-transaction gas limit. Final registration intentionally supplies no certificate hints; +`NitroValidator` succeeds only if the complete chain is already cached and usable. -### Layer 2: AWS CRL Distribution Points +## Final Registration -For intermediates that pass Layer 1, the registrar: +After the cache is ready, the registrar calls: -1. Parses each CRL distribution point from the chain. -2. Validates the URL host against an allowlist requiring the `.amazonaws.com` suffix and the - `nitro-enclave` keyword. HTTP redirects are disabled and responses are bounded to 10 MiB. -3. Fetches the CRL with a configurable timeout. -4. Searches for the certificate's serial number. -5. For each revoked intermediate, submits `NitroEnclaveVerifier.revokeCert(certPathDigest)`. -6. Returns true if any intermediate is revoked, blocking registration for the batch. +```solidity Registration Call +TEEProverRegistry.registerSigner(attestationTbs, signature, hints) +``` -`revokeCert` failures are counted but do not abort registration of other instances on the same -tick. The submitted revocations transition Layer 1 to a hit on the next cycle so subsequent -registrations can short-circuit without re-fetching the CRL. +The registry permits only its owner or manager to call this method. It delegates cryptographic and +certificate validation to `NitroValidator`, then applies Base-specific policy: -## Pending Registration Lifecycle +1. Reject attestations at least 60 minutes old. +2. Reject attestations whose second-level timestamp is greater than or equal to `block.timestamp`. +3. Require PCR0 at index zero, exactly 48 bytes, and not the debug-mode measurement. +4. Require a 65-byte uncompressed secp256k1 public key. +5. Derive the signer as the last 20 bytes of `keccak256(x || y)`. +6. Store the signer as registered and record `keccak256(PCR0)` as its image hash. -Each per-signer pipeline is keyed by Ethereum signer address. The Boundless proof slot for a -signer transitions through: +The registrar uses a 3,300-second default local freshness limit, leaving submission headroom under +the registry's 3,600-second limit. It checks freshness before every certificate, revocation, and +registration transaction so a multi-transaction cold flow stops before submitting stale material. -```mermaid Pending Registration Lifecycle lines wrap expandable -flowchart TB - Start([process_instance]) --> Recover[Recovery scan] - Recover -->|Locked slot| Wait[wait_for_request_fulfillment] - Recover -->|Fulfilled slot| Fresh{Journal fresh?} - Recover -->|All slots Unknown/Expired| Submit[Submit fresh request] - Recover -->|Blocked recovery| Submit +Before each registration attempt, and after ambiguous transaction errors, the registrar reads +`isRegisteredSigner(signer)`. An observed registration is treated as success. Retryable transaction +errors use bounded exponential backoff; reverted receipts and non-retryable errors fail the current +task. - Fresh -->|yes| Receipt[Use recovered receipt] - Fresh -->|no| Submit - Wait --> Receipt - Submit --> Wait +## Certificate Revocation - Receipt --> Send[tx_manager.send registerSigner] - Send -->|Ok| Done([Registered]) - Send -->|Retryable| Send - Send -->|ExecutionReverted| Block[Block recovery for signer] - Block --> Done -``` +`CertManager` maintains a durable revocation set and an immutable pinned AWS Nitro root. The owner +can revoke or unrevoke the root as an emergency halt, set the non-root revoker, and unrevoke +certificate identities. The revoker role can revoke non-root issuer/serial identities. Revocation +is checked during cold verification, cache reuse, and the final cached-chain walk. -A pending recovery state, a fulfilled-but-stale receipt, and an `ExecutionReverted` revert all -funnel back to a fresh submission on the next tick rather than wedging the signer. +For every registration attempt, the registrar checks the pinned root and every planned certificate +against `CertManager.isRevoked()`. A confirmed onchain revocation rejects the registration. -## Onchain Interactions +When CRL fetching is additionally enabled, the registrar: -The registrar uses the following contract calls. `TEEProverRegistry.isValidSigner()` is -intentionally not called by the registrar; that predicate is enforced by `TEEVerifier` at proof -submission time and includes an image-hash match that the registrar cannot satisfy by itself. +1. Fetches CRLs only from allowlisted AWS Nitro hosts, without redirects and with a 10 MiB response + limit. +2. Matches intermediate certificate serial numbers against the CRLs. +3. Calls `CertManager.revokeCert()` for confirmed revocations before rejecting the registration. -| Contract | Method | Caller path | -| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------- | -| [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `registerSigner(output, proof)` | Per-signer registration transaction. | -| [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `deregisterSigner(signer)` | Per-orphan deregistration transaction. | -| [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `isRegisteredSigner(signer)` | Pre-check, post-error reconciliation, orphan race guard. | -| [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `getRegisteredSigners()` | Once per cycle for orphan computation. | -| [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) | `revokeCert(certHash)` | When AWS CRL revokes an intermediate. | -| [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) | `revokedCerts(certHash)` | Layer-1 onchain durable revocation pre-check. | +CRL fetch and parse failures are fail-open and retried on later cycles. Confirmed onchain +revocations always fail closed. -PCR0 enforcement happens onchain at proof submission, not at registration. The registrar registers -any enclave whose Nitro attestation verifies, regardless of its PCR0. This allows the next image's -fleet to be brought up and pre-registered in advance of a hardfork; those signers cannot produce -accepted proposals until the active game implementation's `TEE_IMAGE_HASH` matches their -registered image hash. +## Orphan Deregistration -## Service Lifecycle +After registration task reconciliation, the registrar computes: -At startup, the registrar: +```text Orphan Formula +orphans = registered signers - active signers - pending signers +``` -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 and Boundless wallet balance monitoring. -5. Builds the L1 provider, transaction manager, AWS SDK clients, and discovery client. -6. Builds the registry client and the optional Nitro verifier client. -7. Builds the proof provider for the configured backend. -8. Starts the health server and marks readiness. -9. Starts the driver loop. +It calls `deregisterSigner()` for each orphan only when discovery completed without unresolved +instances and the configured last-known-signer grace period has expired. Deregistration removes the +registered flag and stored image hash. This flow assumes one registrar controls a given registry; +independent registrars would otherwise classify each other's signers as orphans. -The health endpoint reports ready as soon as wiring completes. Connectivity gating is intentionally -omitted because the registrar is outbound-only. +## Upgrade Compatibility -Each driver tick: +The hinted migration upgrades the existing `TEEProverRegistry` proxy implementation. It preserves +the proxy storage layout, owner, manager, game type, proposers, registered signers, and stored signer +image hashes. The active registrar and registry support only the new three-argument hinted API. -1. Discovers instances. -2. Processes instances concurrently. -3. Computes orphans subject to the majority-reachable guard. -4. Submits deregistration transactions for confirmed orphans. +The migration does not change: -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. +- Nitro enclave signer generation or attestation production; +- PCR0-based image selection in `TEEVerifier`; +- `AggregateVerifier` behavior; +- TEE proposal and dispute proof formats; or +- SP1 state-proof behavior. ## Operator Inputs -A registrar needs: - -- L1 RPC endpoint and chain ID. -- `TEEProverRegistry` address. -- AWS region and ALB target group ARN. -- Prover JSON-RPC port shared by the fleet. -- L1 transaction signer (local key, or remote signing endpoint plus expected address). -- Proving backend selection: `boundless` or `direct`. -- For `boundless`: marketplace RPC URL, dedicated wallet key, guest program URL, polling interval, - prove timeout, recovery attempt limit, and attestation freshness window. -- For `direct`: path to the guest ELF. -- Poll interval, prover JSON-RPC timeout, max concurrency, max transaction retries, transaction - retry delay, and the unhealthy registration warm-up window. +A registrar requires: -Optional inputs: +- an L1 RPC endpoint and `TEEProverRegistry` address; +- AWS region and ALB target group ARN; +- the prover JSON-RPC port; +- an L1 transaction signer and transaction-manager limits; +- attestation freshness, polling, timeout, concurrency, cache-grace, and retry settings; and +- health, logging, and Prometheus metrics settings. -- CRL checking enable flag. -- `NitroEnclaveVerifier` address, required when CRL checking is enabled. -- CRL fetch timeout. -- Health server bind address and port. -- Logging filter and Prometheus metrics settings. +No Boundless wallet, marketplace endpoint, RISC Zero program identifier, guest ELF, or proof-backend +selection is used. CRL monitoring is optional and requires the registrar transaction signer to hold +the configured `CertManager` revoker role. In the current CLI, +`--crl-nitro-verifier-address` or `BASE_REGISTRAR_CRL_NITRO_VERIFIER_ADDRESS` enables CRL fetching. +This legacy-named value is only an enable flag; contract discovery still follows the registry to +`NitroValidator` and then `CertManager`. ## Safety Requirements -A registrar implementation must preserve these safety properties: - -- Do not deregister live signers because of a transient AWS or VPC outage. Apply a - majority-reachable guard before any deregistration. -- Treat `Draining` and `Unhealthy` instances as part of the active set as long as their JSON-RPC - endpoint responds, so rotations do not race deregistration. -- Use a fresh random nonce per instance batch and pass it to the enclave attestation request so - the verifier journal carries an unguessable freshness commitment. -- Derive Boundless request slots deterministically from the signer address so a restarted process - can recover in-flight proving work without spending fresh proof costs. -- Reject recovered proofs whose attestation timestamp is older than `max_attestation_age` to keep - recovered proofs strictly inside the onchain `MAX_AGE` window. -- Block recovery for a signer after an `ExecutionReverted` so the next cycle proves freshly - rather than re-submitting the same bad proof. -- 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 earlier in-flight tx cannot cause a redundant - deregistration transaction. -- When CRL checking is enabled, run the onchain durable revocation pre-check before fetching - network CRLs so a previously revoked intermediate cannot be silently rehabilitated. -- 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, transient RPC errors, and Boundless - polling failures as retryable conditions for the next tick rather than as deregistration or - failure signals. +A conforming implementation must preserve these properties: + +- Preserve the exact signed COSE encodings when constructing the attestation TBS. +- Match the attested signer and deterministic nonce before any transaction is submitted. +- Pin the AWS Nitro root and reject malformed, expired, revoked, or parent-mismatched chains. +- Generate hints in the exact verifier order and rely on onchain checks for every supplied inverse. +- Cache certificates parent-first and recover by reading onchain state after every ambiguous result. +- Recheck freshness before each costly transaction in a cold registration. +- Recheck registration state before submission and after ambiguous transaction errors. +- Do not deregister signers while discovery is unresolved or during the configured grace period. +- Keep PCR0 acceptance at proof-submission time so image rotations can be staged safely. +- Do not add an unhinted, ZK, or externally proved runtime fallback to the hinted registration path. diff --git a/docs/upgrades/upcoming-features.mdx b/docs/upgrades/upcoming-features.mdx index 62febb75a..4334df2e6 100644 --- a/docs/upgrades/upcoming-features.mdx +++ b/docs/upgrades/upcoming-features.mdx @@ -17,7 +17,7 @@ Targets are ballpark quarters and may change. A feature moves onto a named upgra | B20 Improvements | Pay fees in B20s, scheduled multiplier updates, and composite policies for the B20 token standard. | [B20 changelog](/specifications/b20/changelog) | Q3 2026 | | Validity Transactions | A transaction type with onchain predicate checks for safer, conditional execution. | [Validity Transactions](/specifications/build-transaction/validity-transactions) | Q3 2026 | | Dynamic Upgrades | An onchain upgrade schedule that lets nodes activate forks live, without a client restart. | [Dynamic Upgrades](/upgrades/cobalt/dynamic-upgrades) | Q3 2026 | -| TEE Migration | Migrate Base infrastructure to run inside a Trusted Execution Environment. | Specification pending | Q3 2026 | +| TEE Registration Migration | Speed up the registration of new TEE signers by replacing external ZK proving with P-384 hints verified onchain. | [Hinted TEE Registration](/specifications/base-protocol/proofs/hinted-tee-registration) | Q3 2026 | | Native Account Abstraction (EIP-8130) | Onchain smart accounts, scoped session keys, and native gas sponsorship, with no bundlers or relays. | [Native Account Abstraction](/specifications/native-account-abstraction) | TBD | ## How Features Ship