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
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).
Background
mpc-contract's on-chain WASM currently bundlesdcap-qvl(and itsring/webpki/x509-certclosure) because it verifies Intel TDX quotes synchronously insidesubmit_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.nearcontract, reachable over a cross-contract Promise.mpc-contractkeeps the post-DCAP business logic (allowlist matching, RTMR3 replay, app-compose validation) and theVerifiedAttestationstorage; only thedcap_qvl::verify::verifycall moves out.Measured impact on the reproducible build:
mainmainbaselineUser 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:
Before / after
Arrows in both diagrams are crate-level Cargo
[dependencies]edges (A → Bmeans 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):The contract's wasm32 build pulls
dcap-qvltransitively throughmpc-attestation → attestation. Verification runs inline insidesubmit_participant_info.After (this stack):
mpc-contract's wasm32 dep graph contains zero paths todcap-qvl. The cryptographic verification happens intee-verifier, invoked over a cross-contract Promise. The post-DCAP body (allowlist matching, RTMR3 replay, app_compose validation) still runs insidempc-contract's callback, but reads only the dcap-qvl-free types fromattestation-typesandtee-verifier-interface.(Off-chain consumers —
tee-authority,attestation-cli,mpc-node— still verify locally; they enable alocal-verifyfeature onmpc-attestationthat brings theattestationcrate +dcap-qvlback into their dep graphs. That feature is dev-dep-only onmpc-contract, so the production WASM stays clean.)The five crates and their roles
The refactor turns one tangled
attestationcrate into five crates with one role each. Three are dcap-qvl-free and form a layered stack; two linkdcap-qvland sit at the edges where verification actually happens.The dcap-qvl-free stack (consumed by
mpc-contract)Each layer depends only on the layer below it:
mpc-attestation → attestation-types → tee-verifier-interface.dcap-qvl?tee-verifier-interfacempc-contractandtee-verifier(it's the shared Promise ABI)mpc-contractandtee-verifier.QuoteBytes,Collateral,VerifiedReport. Deps: onlyborsh+thiserror.attestation-typesmpc-contractonlyDstackAttestation,TcbInfo,Measurements,AppCompose,verify_post_dcap::*(RTMR3 replay, app_compose validation, TCB-status checks, measurement matching). Re-exports theCollateral/QuoteBytesDTOs so callers have a single import path.mpc-attestationlocal-verifyfeature (off-chain consumers only —mpc-contract's lib build does not enable it)mpc-contractonlyAttestation { 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. Thelocal-verifyfeature re-pullsdcap-qvl(viaattestation) for off-chain consumers (tee-authority,attestation-cli,mpc-node).The line between the top two is the most load-bearing:
attestation-typesknows what a TDX attestation looks like;mpc-attestationknows what MPC does with one. If a hypothetical second product wanted to verify Dstack attestations differently, it could depend onattestation-typesalone and skipmpc-attestationentirely.The two dcap-qvl-linking crates (verification happens here)
dcap-qvl?tee-verifierring+webpki+x509-cert)tee-verifier.near/tee-verifier.testnetdcap_qvl::verify::verifyon the DTOs received over a Promise and return aVerifiedReport.dcap_qvl::verify::verify. Depends ontee-verifier-interface.attestationattestation-typesand hosts the one survivingdcap_qvl::verify::verifycall site used bylocal-verifyconsumers.verifycall site. Pulled in only by off-chain consumers (tee-authority,attestation-cli,mpc-node) viampc-attestation'slocal-verifyfeature. Depends onattestation-typesandtee-verifier-interface.After this stack:
mpc-contract's wasm32 dep graph contains zero paths todcap-qvl. The cryptographic check runs intee-verifier; the post-DCAP body (allowlist matching, RTMR3 replay, app_compose validation) still runs insidempc-contract's callback, reading only dcap-qvl-free types fromattestation-typesandtee-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
NEWare introduced in that PR,[dcap-qvl]marks the only crates that linkdcap-qvl, and the goal state at PR #5 is "mpc-contract's WASM no longer reachesdcap-qvl".The line of reasoning, in one paragraph: the goal is the PR #3247 graph —
mpc-contractreaches 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 typempc-contracttouches (#3245 + #3246, which carve the relevant pieces out ofattestationin 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 buildmpc-contractfor wasm32 without transitively pullingdcap-qvlback in.Acceptance Criteria
cargo tree -p mpc-contract --target wasm32-unknown-unknown -e no-proc-macro -i dcap-qvlreturns nothing (or only dev-dep references).mpc-contractWASM drops at least 200 KB below the 1,490,000-byte NEP-509 limit.submit_participant_infoforDstackcompletes via Promise + callback;Mockstays synchronous.pending_attestationsmap empty.Resources & Additional Notes
tee-verifier.near(mainnet),tee-verifier.testnet(testnet); selected at compile time viacfg(feature = "mainnet")onmpc-contract.Collateral/QuoteBytesbetweentee-verifier-interfaceandattestation-types(cosmetic; both have byte-identical Borsh layouts).