Skip to content

docs(l2): add the multiprover (SP1 GPU + TDX) release test - #7243

Open
ilitteri wants to merge 5 commits into
mainfrom
docs/multiprover-release-test
Open

docs(l2): add the multiprover (SP1 GPU + TDX) release test#7243
ilitteri wants to merge 5 commits into
mainfrom
docs/multiprover-release-test

Conversation

@ilitteri

@ilitteri ilitteri commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Every L2 check in the release process exercises a single prover against an OnChainProposer configured to require only that one. A production rollup runs with two, and nothing verified that a batch can satisfy both — so a regression in the two-prover verification path would ship unnoticed.

Description

Adds docs/developers/l2/multiprover-test.md and a release-process checklist entry for a deployment where REQUIRE_SP1_PROOF and REQUIRE_TDX_PROOF are both set, so lastVerifiedBatch only advances once a batch has satisfied both.

The TDX side runs with ETHREX_TDX_DEV_MODE=false on real TDX hardware, so the quote is checked on chain by verifyAndAttestOnChain. This is deliberately stronger than CI, which uses dev mode — there TDXVerifier.register() short-circuits, taking the signing address from the quote's first 20 bytes and never verifying anything. Dev mode is documented as the fallback when no TDX host is available.

Validation

Run against v25.0.0-rc.2, SP1 prover on l2-gpu, everything else on ethrex-tdx-baremetal:

Check Result
TDX_VERIFIER_ADDRESS.isDevMode() false — real verification, not the dev short-circuit
REQUIRE_SP1_PROOF() / REQUIRE_TDX_PROOF() both true
lastVerifiedBatch advanced past zero with both flags set
InvalidTdxProof() reverts none
MRTD() on chain 0x4e2d467d…, the measurement of the image under test
authorizedSignature() set from the verified report, not from raw quote bytes

ProverSetup received for TDXProverSetupACK sent with dev mode off is the verifier accepting a quote signed by that machine's silicon.

Prerequisites the deploy does not handle

Real attestation needed three things, each of which fails opaquely and none of which were documented:

Blocker What is actually wrong
MRTD mismatch _validateReport compares MRTD/RTMR0-2 against constants compiled into TDXVerifier.sol that pin one historical image build; they must be pinned to the image being released
TCB_Cert_Expired (0xea8cd522) nothing has expired — the deploy loads CA.ROOT and CA.PLATFORM but never CA.SIGNING, so the DAO cannot check collateral signatures
Unsupported chain_id: 9 automata-dcap-qpl-tool resolves addresses from a chain-id-keyed registry and cannot serve a dev chain; its exit status is discarded by prepare_quote_prerequisites, so the first symptom is an unrelated revert later

Also recorded: TD_QE identity needs version 4 or 5, not the version inside the JSON (Incorrect_Enclave_Id_Version); these upserts must be sent as raw calldata because cast's tuple parser splits on the commas in the JSON payload; and run-qemu from hypervisor.nix cannot be detached because it hardcodes -serial mon:stdio.

Environment pins

Four more, each hit while bringing the test up:

Pin Symptom when wrong
solc exactly 0.8.31 Source file requires different compiler versionTDXVerifier.sol pins the pragma
fresh chain per deploy deploy-p256 reverts with no reason string (CREATE2 addresses already populated)
--watcher.watch-interval 1000 every commit reverts InvalidPrivilegedTransactionLogs() (0x9e6e5638) on an instantly-mining dev L1
both provers attached before the first batch; L1 and L2 reset together a prover deadlocks on No blocks to prove, asking forever for a batch whose input was pruned, while lastVerifiedBatch cannot advance without it

Follow-ups this surfaced (not in this PR)

  • prepare_quote_prerequisites discards the qpl tool's exit status, so a failed collateral load only surfaces as an unrelated revert much later.
  • The late-joining-prover deadlock is a coordinator robustness gap, not just a test-setup quirk: a prover that restarts after batches accumulate can never catch up.
  • TDXVerifier's measurements are storage variables with no setter, so authorising a new prover image means redeploying the verifier.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.

Every L2 check in the release process exercises one prover against an
`OnChainProposer` that requires only that one. A production rollup runs with
two, and nothing verified that a batch can satisfy both — so a regression in
the two-prover path would have shipped unnoticed.

Adds the runbook and a checklist entry. Validated against v25.0.0-rc.2:
2494 batches verified with `REQUIRE_SP1_PROOF` and `REQUIRE_TDX_PROOF` both
true on chain, real SP1 GPU proofs, and no `InvalidTdxProof` reverts.

The runbook documents four environment pins, each of which fails in a way that
does not point at its cause, and all four were hit while bringing the test up:

- solc must be exactly 0.8.31 (`TDXVerifier.sol` pins the pragma)
- the deploy is not idempotent; it needs a fresh chain
- `--watcher.watch-interval 1000`, or commits revert with
  `InvalidPrivilegedTransactionLogs()` on an instantly-mining dev L1
- both provers must attach before the first batch, and the L1 and L2 datadirs
  must be reset together, or a prover deadlocks asking for a pruned batch

It also records why the TDX guest must be a plain QEMU VM rather than a real
TDX guest: dev-mode `register()` reads the signer from the quote's first 20
bytes, which only holds for a dev quote. Booting a real TDX guest registers
quote-header bytes as the signer and every `verifyBatch` reverts with
`InvalidTdxProof()`, while registration appears to succeed. Real attestation
needs `ETHREX_TDX_DEV_MODE=false`, which the DCAP registry blocks on a dev
chain id — noted as future work rather than papered over.
@ilitteri
ilitteri requested a review from a team as a code owner August 31, 2026 20:46
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

rpc-compat log-bearing cases excluded

Where: KNOWN_EXCLUDED_TESTS in .github/scripts/check-hive-results.sh counts out
eight hive rpc-compat cases — the four eth_getLogs cases, eth_getBlockReceipts/get-block-receipts-latest,
and three eth_getTransactionReceipt cases. They are exactly the cases whose recorded
response contains at least one log object; every case with an empty log array still runs.
Note this leaves eth_getLogs with no rpc-compat coverage at all, since all four of its
cases are in the set.

Why: ethrex populates blockTimestamp on log objects, as geth, besu, nethermind, reth
and erigon all do. hive's rpc-compat compares responses byte-exactly (jsondiff.FullMatch;
the lenient checkJSONStructure path applies only to cases upstream marks speconly), and
the corpus is pinned to execution-apis d08382ae (2025-02-10), whose recordings predate the
field — it entered the schema in execution-apis#639 and the fixtures in #846 (2026-07-22).
So the extra key cannot match, and this is a property of the pin rather than of the response.

The pin cannot move, and this is not temporary. The pin sits one commit before
execution-apis#627, which moved the test chain to a pre-merge genesis: the current corpus has
~36 proof-of-work blocks before its terminal total difficulty. ethrex does not support
pre-merge chains and will not, so importing that chain.rlp fails at block 1 —
validate_block_header has no pre-London base-fee path. Every revision carrying
blockTimestamp in its fixtures also carries that chain, so there is no revision that
satisfies both. Nor can the corpus be patched locally: rpc-compat's Dockerfile clones
ethereum/execution-apis by hard-coded URL, so the branch buildarg cannot point at a fork.

Coverage: the field itself is pinned by
block_timestamp_is_on_the_log_and_not_on_the_receipt in
crates/networking/rpc/types/receipt.rs, which asserts it is present on each log and absent
from the receipt level.

Removal: delete the entries if ethrex ever gains pre-merge chain import, or if upstream
marks these cases speconly so they are type-checked instead of compared byte-for-byte.


The stateless schema id does not identify the encoding

Where: STATELESS_INPUT_SCHEMA_ID in crates/common/types/stateless_ssz.rs.

Upstream keeps the stateless input schema id at 0x1501
(fork_index 0x15 << 8 | revision 0x01) across incompatible body changes. Three
encodings have now shipped under it: tests-zkevm@v0.6.2, then #3248 + #3278,
then #3356, which moved state, codes and public_keys from SszList to
ProgressiveList. ethrex speaks the last one.

The consequence is that the 2-byte prefix cannot be used to detect a stale or
mismatched bundle. A wrong-dialect input is accepted by the id check and then
fails later — in SSZ decode, or on a root that does not match — rather than being
rejected up front for what it is. only_amsterdam_schema_id_decodes therefore
proves less than its name suggests.

Worth raising upstream: a revision field that does not move across a body change
provides no version negotiation at all.


ZisK guest program hash changes with the unsync_cell gate

Where: crates/common/types/block.rs, transaction.rs.

The gate on the single-threaded unsync_cell::OnceCell moved from
all(feature = "eip-8025", target_arch = "riscv64") to
all(feature = "zisk", target_arch = "riscv64") when the eip-8025 feature was removed.

The guest ELFs were previously built --features "<zkvm>-build-elf,ci", which never enabled
eip-8025, so they compiled the atomic once_cell variant. bin/zisk/Cargo.toml does enable
ethrex-common/zisk, so the ZisK guest now compiles the unsafe impl Sync cell instead.
That changes the ELF bytes and therefore the program hash and verification key.

This is intended (the guest is single-threaded, so the unsync cell is sound and cheaper), but it
is a VK change rather than a no-op refactor, and the diffstat presents it as a file rename
(eip8025_cell.rsunsync_cell.rs). Anyone pinning a ZisK VK across this change must
re-register it. The stateless-validator crate now forwards ethrex-common/zisk from its own
zisk feature so the two ZisK guests do not disagree on the cell type.


Release signing key is an unprotected repository secret

Where: .github/workflows/tag_release.yaml.

MINISIGN_SECRET_KEY is a plain repository secret. There is no environment: on
finalize-release or dry-run-release-assets, and gh api repos/lambdaclass/ethrex/rulesets
shows only branch-targeted rulesets, so the github.ref_type == 'tag' condition is a workflow
check rather than an enforced boundary: anyone who can push a tag can reach the signing key.

This is a repository-settings change, not a code change, so it is recorded here rather than
fixed in the tree. Recommended:

  1. Move MINISIGN_SECRET_KEY / MINISIGN_PASSWORD into a GitHub Environment with required
    reviewers, and add environment: to the two jobs that sign.
  2. Add a ruleset targeting refs/tags/v* restricting who may create release tags.

Until then, the compromise of that key is silent and durable: signatures would still verify
against the committed .github/minisign.pub.

@github-actions github-actions Bot added the L2 Rollup client label Aug 31, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

⚠️ Review failed: Kimi API request failed with status 404: Not found the model kimi-k2.5 or Permission denied


Automated review by Kimi (Moonshot AI)

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Medium: The new multiprover procedure does not actually provision a GPU-capable SP1 prover for the documented flows. In docs/developers/l2/multiprover-test.md the host only downloads ethrex-l2-linux-x86_64, but the SP1 prover is later started from docs/developers/l2/multiprover-test.md. The prover backend falls back to CpuProver when the binary is built without the gpu feature, as shown in crates/prover/src/backend/sp1.rs and crates/prover/src/backend/sp1.rs. So the “single GPU host” path silently degrades to CPU proving, and the split-host path never explains how ./ethrex-l2 gets onto l2-gpu. The doc should explicitly require ethrex-l2-linux-x86_64-gpu on the GPU host and state where to run it from.

  2. Low: Step 1 is not copy-paste safe because bin/ is never created before writing solc into it. docs/developers/l2/multiprover-test.md will fail on a clean directory with “No such file or directory”, and the following chmod then fails too. Add mkdir -p bin before the curl -o bin/solc ....

  3. Low: The release checklist snippet uses an undefined $L1_RPC variable. docs/developers/release-process.md and docs/developers/release-process.md will pass an empty RPC URL unless the reader has already exported it elsewhere. Either use http://localhost:8545 directly here or add the required export L1_RPC=... immediately above.

No code-level correctness or security findings beyond the operational/documentation issues above; the diff is documentation-only. I did not execute the full procedure.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

The prose was hard-wrapped at ~80 columns, the way a commit body is written.
Every other page under docs/developers keeps one unwrapped line per paragraph
and lets the renderer do the wrapping, so this stood out in the source and
produced needless diff noise on any future edit.

Reflowed the paragraphs, bullets and blockquotes in the new page and in the
release-process section it adds. Code blocks and tables are untouched.

Two things the reflow surfaced are fixed as well: the environment-pin list
said "three things" after a fourth had been added, and the first procedure
step was still called "Prepare the TDX host" even though the test runs against
a plain guest and needs no TDX-capable CPU.
The checklist item did not say where to run it, unlike every other entry in
that list, which names its host. It now names both: the SP1 prover on
`l2-gpu`, and the L1, deploy, sequencer and TDX prover VM on
`ethrex-tdx-baremetal`.

The runbook said "any Linux host with QEMU/KVM" for the TDX side, which is
true of the dev-mode configuration but is not the operational answer.
`ethrex-tdx-baremetal` is the designated host and the one where extending this
to real attestation would happen, so the page names it, keeps the note that
dev mode does not depend on its TDX hardware, and names both hosts in the step
headings.

Also notes that both machines are shared, so the stack should be torn down
afterwards, and that on `l2-gpu` this test contends with the SP1 GPU
integration test for the same GPU, ports and datadirs.
The test was written around `ETHREX_TDX_DEV_MODE=true`, which is what CI uses.
In that mode `TDXVerifier.register()` short-circuits: it takes the signing
address from the quote's first 20 bytes and never calls
`verifyAndAttestOnChain`. So the quote is trusted rather than verified, and
running it on TDX hardware buys nothing — the guest even has to be a plain VM,
because a real quote's header bytes are not an address.

Switch it to `ETHREX_TDX_DEV_MODE=false` with a real TDX guest, so a quote
signed by the host's silicon is verified on chain. Confirmed working on
`ethrex-tdx-baremetal`: `ProverSetup received for TDX` followed by
`ProverSetupACK sent` with dev mode off is the verifier accepting a genuine
quote.

Three prerequisites the deploy does not handle, each of which fails opaquely:

- `_validateReport` compares MRTD and RTMR0-2 against constants compiled into
  `TDXVerifier.sol`, which pin one historical image; the release under test
  measures differently and registration reverts with `MRTD mismatch`. Pin them
  to the image being released, read out of a real quote.
- `CA.SIGNING` — the Intel TCB Signing certificate — is never loaded, only
  `CA.ROOT` and `CA.PLATFORM`, so the DAO cannot check collateral signatures
  and rejects the upsert with `TCB_Cert_Expired`. Nothing has expired.
- The TCB info and QE identity for the platform have to be upserted. ethrex
  shells out to `automata-dcap-qpl-tool` for this, which resolves addresses
  from a chain-id-keyed registry and rejects the dev L1 with
  `Unsupported chain_id: 9` — silently, since its exit status is discarded.
  Fetch them from the host's PCCS service and upsert them directly.

Also records that TD_QE identity needs version 4 or 5 rather than the version
inside the JSON, that these calls have to be sent as raw calldata because
cast's tuple parser splits on the commas in the JSON payload, and that
`run-qemu` from `hypervisor.nix` cannot be detached because it hardcodes
`-serial mon:stdio`.

Dev mode stays documented as the fallback when no TDX host is available.
…rover test

Both proofs are required per batch, so verification advances at the slower
prover's rate. An SP1 proof on the GPU host measures a steady 106s, so the 15s
commit time the runbook carried outran proving by roughly 7x and the gap never
closed: a 63-hour run reached batch 10983 committed against 2127 verified.

The integration suite's withdrawal tests block until the batch holding the
withdrawal is verified, so in that state step 9 cannot pass however long it
runs. Raises the commit time to 120s, says why, and adds the check to step 9
plus a troubleshooting row for the symptom it produces.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L2 Rollup client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant