Skip to content

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

@pbeza

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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions