Skip to content

Move dcap-qvl out of the mpc-contract WASM #3264

Description

@pbeza

Background

mpc-contract's on-chain WASM currently bundles dcap-qvl (and its ring / webpki / x509-cert closure) because it verifies Intel TDX quotes synchronously inside submit_participant_info. That closure dominates the contract's binary size and pushes it close to the NEP-509 1,490,000-byte hard limit, leaving little headroom for future state-migration changes.

This work splits cryptographic quote verification out into a separate tee-verifier.near contract, reachable over a cross-contract Promise. mpc-contract keeps the post-DCAP business logic (allowlist matching, RTMR3 replay, app-compose validation) and the VerifiedAttestation storage; only the dcap_qvl::verify::verify call moves out.

Measured impact on the reproducible build:

Bytes Delta from main
main baseline 1,459,158
After this stack (#3247) 1,149,708 −309,450 (−21.2%)

User Story

As an MPC contract maintainer, I want quote verification to live outside mpc-contract's WASM so that future on-chain features and state migrations have safe headroom under the NEP-509 size limit and the contract's hot path is no longer coupled to dcap-qvl upgrades.

How the work is split

The split follows a single principle:

Only the verifier contract and the off-chain attestation crate are allowed to link dcap-qvl. Every other crate stays dcap-qvl-free, including the wire types they exchange.

Before / after

Arrows in both diagrams are crate-level Cargo [dependencies] edges (A → B means A depends on B). The Promise call is drawn separately because it's a runtime cross-contract call, not a Cargo dep.

Before (today's main):

                       ┌────────────────┐
                       │  mpc-contract  │
                       │     (WASM)     │
                       └────────┬───────┘
                                │
                                ▼
                       ┌────────────────┐
                       │ mpc-attestation│
                       └────────┬───────┘
                                │
                                ▼
                       ┌────────────────┐
                       │   attestation  │
                       │  ── links ──   │ ─────────┐
                       │  dcap-qvl      │          │
                       └────────────────┘          ▼
                                          ┌────────────────┐
                                          │   dcap-qvl     │ ← in WASM,
                                          │ + ring         │   ~310 KB
                                          │ + webpki       │   of binary
                                          │ + x509-cert    │
                                          └────────────────┘

The contract's wasm32 build pulls dcap-qvl transitively through mpc-attestation → attestation. Verification runs inline inside submit_participant_info.

After (this stack):

                       ┌────────────────┐
                       │  mpc-contract  │ ─── Promise ───┐
                       │     (WASM)     │                │
                       └────────┬───────┘                │
                                │                        │
                                ▼                        ▼
       ┌─────────────────────────────────────┐  ┌───────────────────┐
       │ mpc-attestation, attestation-types, │  │   tee-verifier    │
       │     tee-verifier-interface          │  │    (contract)     │
       │  ─────── no dcap-qvl ──────         │  │  ── links ──      │
       └─────────────────────────────────────┘  │   dcap-qvl        │
                                                └─────────┬─────────┘
                                                          │
                                                          ▼
                                                ┌───────────────────┐
                                                │     dcap-qvl      │ ← in a
                                                │  + ring/webpki/   │   separate
                                                │    x509-cert      │   contract,
                                                └───────────────────┘   not in
                                                                        mpc-contract
                                                                        WASM

mpc-contract's wasm32 dep graph contains zero paths to dcap-qvl. The cryptographic verification happens in tee-verifier, invoked over a cross-contract Promise. The post-DCAP body (allowlist matching, RTMR3 replay, app_compose validation) still runs inside mpc-contract's callback, but reads only the dcap-qvl-free types from attestation-types and tee-verifier-interface.

(Off-chain consumers — tee-authority, attestation-cli, mpc-node — still verify locally; they enable a local-verify feature on mpc-attestation that brings the attestation crate + dcap-qvl back into their dep graphs. That feature is dev-dep-only on mpc-contract, so the production WASM stays clean.)

The five crates and their roles

The refactor turns one tangled attestation crate into five crates with one role each. Three are dcap-qvl-free and form a layered stack; two link dcap-qvl and sit at the edges where verification actually happens.

The dcap-qvl-free stack (consumed by mpc-contract)
   ┌─────────────────────────────────────────┐
   │ mpc-attestation     (MPC product API)   │  ← what MPC does with an attestation
   ├─────────────────────────────────────────┤
   │ attestation-types   (TDX/Dstack domain) │  ← what a TDX attestation IS
   ├─────────────────────────────────────────┤
   │ tee-verifier-iface  (Promise wire DTOs) │  ← DTOs crossing the contract boundary
   └─────────────────────────────────────────┘

Each layer depends only on the layer below it: mpc-attestation → attestation-types → tee-verifier-interface.

Crate Links dcap-qvl? Linked into which contract's WASM? Role — one sentence Concretely owns
tee-verifier-interface no both mpc-contract and tee-verifier (it's the shared Promise ABI) Defines the Borsh-encoded DTOs that cross the Promise boundary between mpc-contract and tee-verifier. The three DTOs: QuoteBytes, Collateral, VerifiedReport. Deps: only borsh + thiserror.
attestation-types no mpc-contract only Defines what a TDX/Dstack attestation is and how to verify its post-DCAP invariants — vendor-neutral, no MPC concepts. DstackAttestation, TcbInfo, Measurements, AppCompose, verify_post_dcap::* (RTMR3 replay, app_compose validation, TCB-status checks, measurement matching). Re-exports the Collateral / QuoteBytes DTOs so callers have a single import path.
mpc-attestation no by default; yes with the optional local-verify feature (off-chain consumers only — mpc-contract's lib build does not enable it) mpc-contract only Defines what MPC does with an attestation: the Attestation { Dstack, Mock } enum, the (tls_pk, account_pk) binding, and the two-step API the Promise+callback flow needs. Attestation { Dstack, Mock }, ReportData::V1(tls_pk, account_pk), extract_dcap_inputs, finish_verify. The local-verify feature re-pulls dcap-qvl (via attestation) for off-chain consumers (tee-authority, attestation-cli, mpc-node).

The line between the top two is the most load-bearing: attestation-types knows what a TDX attestation looks like; mpc-attestation knows what MPC does with one. If a hypothetical second product wanted to verify Dstack attestations differently, it could depend on attestation-types alone and skip mpc-attestation entirely.

The two dcap-qvl-linking crates (verification happens here)
Crate Links dcap-qvl? Linked into which contract's WASM? Role — one sentence Concretely owns
tee-verifier yes (plus ring + webpki + x509-cert) this is the contract — tee-verifier.near / tee-verifier.testnet The new standalone contract whose entire job is to call dcap_qvl::verify::verify on the DTOs received over a Promise and return a VerifiedReport. Stateless wrapper around dcap_qvl::verify::verify. Depends on tee-verifier-interface.
attestation yes neither — off-chain only, never enters any contract's WASM The legacy crate, reduced to a thin off-chain shim: re-exports attestation-types and hosts the one surviving dcap_qvl::verify::verify call site used by local-verify consumers. The off-chain verify call site. Pulled in only by off-chain consumers (tee-authority, attestation-cli, mpc-node) via mpc-attestation's local-verify feature. Depends on attestation-types and tee-verifier-interface.

After this stack: mpc-contract's wasm32 dep graph contains zero paths to dcap-qvl. The cryptographic check runs in tee-verifier; the post-DCAP body (allowlist matching, RTMR3 replay, app_compose validation) still runs inside mpc-contract's callback, reading only dcap-qvl-free types from attestation-types and tee-verifier-interface.

Why this becomes five PRs

The split follows the dep graph above: each PR is the smallest reviewable move that brings the next required piece online without breaking the build. The timeline below shows the dep graph after each PR — boxes marked NEW are introduced in that PR, [dcap-qvl] marks the only crates that link dcap-qvl, and the goal state at PR #5 is "mpc-contract's WASM no longer reaches dcap-qvl".

─── PR #3235 ──────────────────────────────────────────────────────────────
feat(tee-verifier-interface): Borsh DTOs for the verifier contract boundary

   mpc-contract ─▶ mpc-attestation ─▶ attestation [dcap-qvl] ─▶ dcap-qvl

   ┌─ NEW ────────────────────────┐
   │ tee-verifier-interface       │   (unused by anyone yet — that's fine)
   └──────────────────────────────┘

   What this unlocks: a shared place for the Promise call's DTOs to live.
   Either side of #3237 would otherwise have to define them, forcing the
   other side to link the host's deps.

─── PR #3237 ──────────────────────────────────────────────────────────────
feat(tee-verifier): stateless dcap_qvl::verify wrapper contract

   mpc-contract ─▶ mpc-attestation ─▶ attestation [dcap-qvl] ─▶ dcap-qvl

   ┌─ NEW ────────────────────────┐
   │ tee-verifier [dcap-qvl]      │ ─▶ tee-verifier-interface
   │   (its own WASM)             │ ─▶ dcap-qvl
   └──────────────────────────────┘

   What this unlocks: the verifier contract exists as its own WASM,
   small enough to audit and deploy independently of the mpc-contract
   refactor. mpc-contract still verifies inline — nothing wired up yet.

─── PR #3245 ──────────────────────────────────────────────────────────────
refactor(attestation): extract attestation-types crate without dcap-qvl

   mpc-contract ─▶ mpc-attestation ─▶ attestation [dcap-qvl] ─▶ dcap-qvl
                                            │
                                            ▼  (re-exports for compat)
                                   ┌─ NEW ────────────────────┐
                                   │ attestation-types        │
                                   │   verify_post_dcap::*,   │
                                   │   Measurements, TcbInfo, │
                                   │   ReportData             │
                                   └──────────────────────────┘

   What this unlocks: the post-DCAP helpers and TDX domain types now
   live in a dcap-qvl-free crate. Behavior-preserving — attestation
   re-exports the moved modules so every existing caller keeps compiling.

─── PR #3246 ──────────────────────────────────────────────────────────────
refactor: move DstackAttestation, Collateral, QuoteBytes out of `attestation`
          into their respective dcap-qvl-free homes

   mpc-contract ─▶ mpc-attestation ─▶ attestation [dcap-qvl] ─▶ dcap-qvl
                                            │  (loses these 3 types)
                                            │
                                            ├── DstackAttestation ──▶ attestation-types
                                            │                                │
                                            │                                │ NEW edge
                                            │                                ▼
                                            └── Collateral, QuoteBytes ─▶ tee-verifier-interface
                                                                        (re-exported via
                                                                         attestation-types
                                                                         for a single
                                                                         import path)

   What this unlocks: every type mpc-contract touches now lives in a
   dcap-qvl-free crate. Collateral / QuoteBytes are no longer duplicated
   between the verifier interface and the attestation crate. Last
   precondition before #3247 can compile mpc-contract for wasm32
   without dcap-qvl.

─── PR #3247 ──────────────────────────────────────────────────────────────
feat(mpc-contract): drop dcap-qvl from WASM via Promise+callback verifier
                    integration

   mpc-contract ─▶ mpc-attestation ─▶ attestation-types ─▶ tee-verifier-iface
        │                                                          ▲
        │ Promise (runtime, not Cargo)                              │
        ▼                                                           │
   tee-verifier [dcap-qvl] ────────────────────────────────────────┘

   ┌─ off-chain consumers only (tee-authority, attestation-cli, mpc-node):
   │   mpc-attestation --features local-verify ─▶ attestation [dcap-qvl]
   └─ mpc-contract's lib build does NOT enable local-verify.

   What this unlocks: submit_participant_info's Dstack arm splits into
   extract_dcap_inputs (before the Promise) and finish_verify (in the
   callback). mpc-contract's wasm32 dep graph contains zero paths to
   dcap-qvl. This is the PR where the WASM size actually drops.
───────────────────────────────────────────────────────────────────────────

The line of reasoning, in one paragraph: the goal is the PR #3247 graph — mpc-contract reaches only dcap-qvl-free crates, and the cryptographic check runs in a separate contract over a Promise. To compile that graph, three things must exist first: the shared DTO crate (#3235), the verifier contract that uses them (#3237), and a dcap-qvl-free home for every type mpc-contract touches (#3245 + #3246, which carve the relevant pieces out of attestation in two behavior-preserving steps so each diff stays reviewable). #3247 then becomes the rewiring step — small enough to read line-by-line — and is the one that actually drops the WASM size. Steps #3245 and #3246 are the unglamorous middle: they're the precondition for #3247 to compile, not cosmetic cleanup. Without them, #3247 cannot build mpc-contract for wasm32 without transitively pulling dcap-qvl back in.

Acceptance Criteria

  • cargo tree -p mpc-contract --target wasm32-unknown-unknown -e no-proc-macro -i dcap-qvl returns nothing (or only dev-dep references).
  • Reproducible mpc-contract WASM drops at least 200 KB below the 1,490,000-byte NEP-509 limit.
  • submit_participant_info for Dstack completes via Promise + callback; Mock stays synchronous.
  • Allowlists are re-read fresh inside the callback (governance votes mid-flight take effect immediately).
  • A failed verifier Promise refunds the caller's attached deposit and clears the pending entry.
  • State migration initializes the new pending_attestations map empty.

Resources & Additional Notes

  • NEP-509 transaction size limit: 1,490,000 bytes.
  • Verifier account IDs: tee-verifier.near (mainnet), tee-verifier.testnet (testnet); selected at compile time via cfg(feature = "mainnet") on mpc-contract.
  • Follow-up work tracked separately: sandbox integration test deploying both contracts, e2e tests, deduplication of Collateral / QuoteBytes between tee-verifier-interface and attestation-types (cosmetic; both have byte-identical Borsh layouts).

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions