feat(contract): async TEE attestation verification, drop dcap-qvl - #3714
Conversation
Dstack attestations are no longer verified in-WASM. submit_participant_info now branches: Mock is verified synchronously, Dstack yields and offloads DCAP quote verification to a separate tee-verifier contract via a cross-contract call, resuming through resolve_verification / on_attestation_verified. This lets the contract drop the mpc-attestation local-verify feature (and with it dcap-qvl). Adds the pending-attestation state, the yield/resume + refund/revert machinery, the verifier-call gas config, and the related error variants. This is the contract-side wiring only; the verifier interface (tee-verifier-interface, DstackAttestation::verify) already exists. Sandbox coverage (stub verifier + tests) and the design doc follow in a separate PR.
There was a problem hiding this comment.
Pull request overview
This PR wires the MPC contract to verify Attestation::Dstack asynchronously by offloading DCAP quote verification to an external tee-verifier contract (via a cross-contract verify_quote call), while keeping Attestation::Mock synchronous. It also removes the dcap-qvl dependency from mpc-contract by dropping the mpc-attestation/local-verify feature and updates the interface/config/snapshots to reflect the new async flow.
Changes:
- Implement async Dstack attestation submission using yield/resume (
resolve_verification→on_attestation_verified) with refund + cleanup on rejection/timeout. - Add pending-attestation state and new config gas fields + interface constants for verifier/callback/failure calls.
- Remove contract-side local DCAP verification dependency path and regenerate ABI/Borsh schema snapshots and affected tests.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/test-utils/src/contract_types.rs | Updates dummy config builder to populate newly added gas config fields. |
| crates/near-mpc-contract-interface/src/types/config.rs | Extends InitConfig/Config with verifier + callback + failure gas parameters and updates related tests. |
| crates/near-mpc-contract-interface/src/method_names.rs | Adds method-name constants for new callbacks and the verifier’s verify_quote entrypoint. |
| crates/mpc-attestation/src/attestation.rs | Updates docs around local verification usage after moving contract-side DCAP verification out-of-WASM. |
| crates/contract/tests/snapshots/abi__abi_has_not_changed.snap | Regenerates ABI snapshot to include new private callbacks, types, and config fields. |
| crates/contract/tests/sandbox/upgrade_from_current_contract.rs | Updates sandbox upgrade test config values to include new fields. |
| crates/contract/tests/sandbox/contract_configuration.rs | Updates initialization/configuration sandbox test to include new config fields. |
| crates/contract/tests/inprocess/attestation_submission.rs | Adjusts in-process tests to match new attestation-submission error plumbing. |
| crates/contract/src/v3_12_0_state.rs | Adds pending_attestations initialization to the v3.12.0 → current state migration. |
| crates/contract/src/tee/tee_state.rs | Splits mock vs Dstack verification paths, adds revert support for async storage-charge failures, and exposes attestation submission errors. |
| crates/contract/src/tee/pending_attestation.rs | Introduces new on-chain state/type for in-flight Dstack attestation verification and yield resume result. |
| crates/contract/src/tee.rs | Exposes the new pending_attestation module. |
| crates/contract/src/storage_keys.rs | Adds a storage key variant for the pending-attestations map. |
| crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap | Regenerates Borsh schema snapshot for new config fields and added contract state. |
| crates/contract/src/lib.rs | Implements async Dstack submission flow, pending state management, refund/fail machinery, and storage charging refactor. |
| crates/contract/src/errors.rs | Adds new tee error variants and plumbs AttestationSubmissionError into the top-level Error. |
| crates/contract/src/dto_mapping.rs | Maps new config fields between DTOs and internal config. |
| crates/contract/src/config.rs | Adds default values for new gas config fields. |
| crates/contract/Cargo.toml | Drops mpc-attestation/local-verify (removing dcap-qvl from contract), adds tee-verifier-interface, and adjusts ABI features. |
| Cargo.lock | Updates dependency graph for the contract changes (new deps, removed feature linkage). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
test |
Pull request overviewSplits the contract-side async TEE attestation flow out of #3664. FindingsBlocking (must fix before merge):
Non-blocking (nits, follow-ups, suggestions):
|
- refund the attached deposit when a participant refreshes an existing attestation (charge_attestation_storage early-return kept it silently) - add v3_13_0_state migration shadow so migrate() can upgrade from a deployed 3.13.0 layout, not just 3.12.0 - collapse the two per-arm debug_asserts in on_attestation_verified into one guarding both resolved paths, with a note on why cleanup stays in resolve_verification (its remove-before-store gates the timeout race) - nits: from_yoctonear(0) on the fail-call; drop a redundant yield comment; reframe the promise_yield_resume comment; PendingAttestation fields pub(crate); document the verify_quote wire args; benchmark TODOs on the new gas defaults - TODO(#3720): stash only tcb_info in PendingAttestation (follow-up)
dcap-qvl
Rename the attestation verify-and-store helpers so they describe what they do rather than when they run: - add_mock_participant -> verify_and_store_mock - finish_dstack_verify -> verify_and_store_dstack - finish_verified_attestation -> verify_post_dcap_and_store Also tighten the resolve_verification and on_attestation_verified comments: scope the no-verdict note to the Err arm, name the ~200-block yield-resume timeout explicitly, and trim the debug_assert rationale to the single invariant it rests on
|
@claude review |
Pull request overviewSplits the async TEE attestation core out of #3664. Changes:
Reviewed changesPer-file summary
FindingsBlocking issues from my prior review have been addressed in this iteration:
Non-blocking (nits, follow-ups, suggestions):
✅ Approved |
- Fix broken rustdoc intra-doc link in tee_state.rs (Attestation was not in scope; use the in-scope DstackAttestation), which the workspace broken_intra_doc_links=deny lint would fail CI on - Correct the v3_13_0_state module doc field count (four Config gas fields plus the pending_attestations map, not two) - Rename stale add_participant_* tests to the verify_and_store_mock SUT and the <sut>__should_<assertion> form - Fix a/an grammar in an Attestation::Dstack doc comment
The a->an grammar fix in on_attestation_verified's doc comment changed the doc string embedded in the ABI, which test_abi_has_not_changed compares against the committed snapshot. Update the snapshot to match
The fail_on_timeout callback sites used NearToken::from_near(0) while the new attestation code uses from_yoctonear(0); both are zero. Unify on from_yoctonear(0)
…-participants submit_participant_info required a deposit unconditionally, which broke nodes signing with a function-call access key: such keys cannot attach any deposit, so every re-attestation failed at tx validation with DepositWithFunctionCall. Restore the pre-#3714 conditional: a participant re-attesting an existing entry attaches no deposit and is charged nothing (so the node's function-call key can re-attest), while a new entry or a non-participant caller still pays the storage delta. The client and node stop attaching the deposit; an operator's full-access key funds a first-time (join) submission. Adds a sandbox regression test that drives submit_participant_info through a real function-call access key (zero deposit succeeds; a deposit is rejected). Closes #3925
… onboarding The fixed MINIMUM_ATTESTATION_STORAGE_DEPOSIT floor rejected zero-deposit submissions outright. Restore the pre-#3714 behavior: charge only the measured storage delta and reject only when the attached deposit is below it. A participant re-attesting an unchanged entry grows nothing, so it pays nothing and its function-call key works; a new entry costs the measured delta, funded by a deposit-capable caller. In e2e, fund each new participant's first attestation before resharing, signed with the node's near_signer_key so the stored account_public_key matches the key the node later uses for key-event votes (an operator key would store the wrong key and stall resharing). Add MpcNodeState::near_signer_key for this. Repurpose the worst-case test to assert the entry cost stays under a bounded ceiling, and regenerate the ABI snapshot.
submit_participant_info takes no deposit; the contract's own balance stakes the bounded attestation entry. A node self-submits with its function-call access key for its first attestation and re-attestations alike, fixing onboarding: #3714 required a deposit, which a function-call key cannot attach. This makes intentional what pre-#3714 did by accident. Pre-#3714 measured the storage delta right after inserting into the IterableMap, before the deferred write was flushed, so the delta read as 0 and the caller was never charged. #3714 added the flush (kept here), which exposed the real cost and started rejecting the node's zero-deposit submit. Storage is bounded and reclaimed by clean_invalid_attestations, so the contract-funded cost is bounded and self-healing. Removes the charge path, the payable modifiers, deposit forwarding, and the e2e funding workaround; updates tests and the ABI snapshot.
…ragraph The background implied contract-funded storage started with #3940. It did not: the deployed 3.13.0 contains charging code that never collects, because it reads the storage delta before the write is flushed, and #3714 -- the only version that genuinely required a deposit -- never shipped. So this has been true of every deployed version, and #3940 only made it intentional. That matters because it means the drain is not a regression to be reverted but a gap that was always there. Also split the run-on sentence about the security implication into three bullets: unlimited entries, roughly 7x cost asymmetry, and the contract being unable to write state at all once its balance is gone.
Closes #3642.
Splits the first half of #3664 out for focused review: the contract-side async TEE attestation flow and the removal of
dcap-qvlfrommpc-contract. The stub verifier, sandbox tests, and design doc land in a stacked PR on top of this branch.Today the MPC contract verifies a node's Intel TDX attestation synchronously, inside its own WASM, by linking the
dcap-qvllibrary. This PR moves that crypto verification out to a separatetee-verifiercontract and makessubmit_participant_infoasynchronous (yield/resume via a cross-contract call, with refund/revert on failure or the ~200-block timeout). The MPC contract stops linkingdcap-qvlentirely.WASM size impact
Dropping
dcap-qvlshrinks the contract WASM by ~310 KB (-20.5%). Both binaries built identically (cargo near build non-reproducible-wasm --features abi --profile=release-contract --locked, samewasm-opt -Opost-step):maincargo tree -p mpc-contract -i dcap-qvlreturns no match on this branch (it resolves onmain), confirmingdcap-qvlis no longer in the contract's build graph.