Skip to content

feat(contract): async TEE attestation verification, drop dcap-qvl#3714

Open
pbeza wants to merge 10 commits into
mainfrom
3642-async-attestation-core
Open

feat(contract): async TEE attestation verification, drop dcap-qvl#3714
pbeza wants to merge 10 commits into
mainfrom
3642-async-attestation-core

Conversation

@pbeza

@pbeza pbeza commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes #3642.

Splits the first half of #3664 out for focused review: the contract-side async TEE attestation flow and the removal of dcap-qvl from mpc-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-qvl library. This PR moves that crypto verification out to a separate tee-verifier contract and makes submit_participant_info asynchronous (yield/resume via a cross-contract call, with refund/revert on failure or the ~200-block timeout). The MPC contract stops linking dcap-qvl entirely.

WASM size impact

Dropping dcap-qvl shrinks the contract WASM by ~296 KB (-19.6%). Both binaries built identically (cargo near build non-reproducible-wasm --features abi --profile=release-contract --locked, same wasm-opt -O post-step):

Raw bytes Gzipped
main (v3.13.0) 1,510,735 575,434
this PR 1,214,333 455,515
delta -296,402 (-19.6%) -119,919 (-20.8%)

cargo tree -p mpc-contract -i dcap-qvl returns no match on this branch (it resolves on main), confirming dcap-qvl is no longer in the contract's build graph.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_verificationon_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.

Comment thread crates/contract/src/tee/pending_attestation.rs Outdated
Comment thread crates/contract/src/lib.rs Outdated
@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

test

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

Pull request overview

Splits the contract-side async TEE attestation flow out of #3664. submit_participant_info now branches by attestation type: Mock keeps the synchronous in-WASM path, while Dstack registers a yield, offloads DCAP verification to a separate tee-verifier contract via a cross-contract call, and resumes through resolve_verification / on_attestation_verified (refunding deposit on rejection / yield-timeout). The contract drops mpc-attestation/local-verify (and with it dcap-qvl). Adds the pending-attestation map, four new gas knobs, and VerificationAlreadyPending / VerifierNotConfigured errors.

Findings

Blocking (must fix before merge):

  • crates/contract/src/config.rs:71 -- fail_attestation_submission_tera_gas is inserted between existing fields (fail_on_timeout_tera_gas and clean_tee_status_tera_gas) rather than appended. Combined with the new pending_attestations field appended to MpcContract, this changes the borsh layout of both Config and MpcContract. The migration in lib.rs:2161 only handles v3_12_0_state; the fallback try_state_read::<Self>() cannot deserialize state written by the current 3.13.0 release (CHANGELOG 2026-06-24), which has remove_non_participant_tee_verifier_votes_tera_gas, tee_verifier_account_id, and tee_verifier_votes but neither pending_attestations nor the inserted fail_attestation_submission_tera_gas. If 3.13.0 has been deployed anywhere (mainnet/testnet/devnet), this PR's migrate() will fail on first invocation. Either (a) append fail_attestation_submission_tera_gas at the end of Config and add a v3_13_0_state.rs snapshot describing the released 3.13.0 layout, or (b) confirm in the PR description that 3.13.0 has not been deployed anywhere.

  • crates/contract/src/lib.rs:842-911 -- engineering-standards.md "Add tests": the PR removes the only unit tests covering Dstack submission (test_submit_participant_info_succeeds_with_valid_dstack_attestation, test_submit_participant_info_fails_without_approved_mpc_hash, test_tee_attestation_fails_with_invalid_tls_key) and ships no unit-level coverage for the new async machinery. The following invariants are unit-testable here and would fail if reverted:

    • submit_participant_info returns VerifierNotConfigured when tee_verifier_account_id.is_none().
    • submit_participant_info returns VerificationAlreadyPending when a PendingAttestation already exists for the account.
    • submit_participant_info (Dstack path) inserts a PendingAttestation keyed by account id.
    • TeeState::revert_dstack_store restores the displaced entry on UpdatedExistingParticipant(_) and removes the new entry on NewlyInsertedParticipant.
    • charge_attestation_storage returns InsufficientDeposit when attached < cost and does not schedule a partial refund on that branch.

    Please add these before merge; sandbox coverage in the follow-up PR is complementary, not a substitute.

Non-blocking (nits, follow-ups, suggestions):

  • crates/contract/src/lib.rs:2503 vs lib.rs:878 -- NearToken::from_near(0) vs NearToken::from_yoctonear(0). Pick one.
  • crates/contract/src/lib.rs:821 -- caller_is_participant = self.voter_account().is_ok() quietly conflates "not signer", "auth failed", and "not a participant" into a single bool. A dedicated caller_is_attested_participant() helper (or a one-line note pinning the dependency) would prevent silent regressions if voter_account ever changes meaning.
  • crates/contract/src/lib.rs:907-909 -- comment restates the helper docstring at lib.rs:329-330. Could be deleted.
  • crates/contract/src/lib.rs:2417-2418 -- "MUST be the last host call: anything after could panic..." is slightly misleading since promise_yield_resume returns bool and never panics; the real risk is the preceding serde_json::to_vec(...).expect(...). Consider reframing.
  • crates/contract/src/lib.rs:2474-2507 -- on_attestation_verified's Ok(AttestationResult::Err(reason)) branch silently relies on resolve_verification having already cleaned up. A debug_assert!(!self.pending_attestations.contains_key(&account_id)) in the Ok(_) arms (or an inline note that resolve_verification owns cleanup on the resolved-with-error path) would prevent a future change on either side from silently leaking a pending entry.
  • crates/contract/src/tee/pending_attestation.rs:14-30 -- PendingAttestation's fields are pub even though the only consumer is crate::lib. Consider pub(crate).
  • crates/contract/src/lib.rs:875-880 -- a one-line pointer at tee_verifier_interface::verify_quote (or method_names::VERIFY_QUOTE) would make the implicit two-positional-args borsh wire contract discoverable from this call site.
  • Documentation alignment: if any pre-existing design doc on main describes in-WASM verification, please flag/update it in the same PR per CLAUDE.md "Documentation alignment".
  • Gas defaults (config.rs:40,43,47): DEFAULT_VERIFIER_TERA_GAS = 100, DEFAULT_RESOLVE_VERIFICATION_TERA_GAS = 60, DEFAULT_ON_ATTESTATION_VERIFIED_TERA_GAS = 10 are reasonable upper-bound guesses but unbenchmarked at this layer; mirror the cleanup_orphaned_node_migrations_tera_gas TODO (config.rs:32) with a TODO(#issue): benchmark note so the work is tracked once the stub verifier lands.

⚠️ Issues found

pbeza added 2 commits July 1, 2026 12:50
- 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)
@pbeza pbeza changed the title feat(contract): async TEE attestation verification, drop dcap-qvl feat(contract): async TEE attestation verification, drop dcap-qvl Jul 1, 2026
pbeza added 3 commits July 1, 2026 15:00
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 1 comment.

Comment thread crates/contract/src/lib.rs
@pbeza

pbeza commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown

Pull request overview

Splits the async TEE attestation core out of #3664. submit_participant_info now branches on the attestation variant: Mock keeps the synchronous in-WASM path (verify_and_store_mock + charge_attestation_storage), while Dstack schedules a verify_quote cross-contract call to the voted-in tee-verifier, registers a yield, and resumes via resolve_verification / on_attestation_verified (refunding the attached deposit and failing from a separate receipt on rejection or timeout). The contract stops linking dcap-qvl (drops mpc-attestation/local-verify), which is what produces the ~296 KB WASM shrink in the PR description. Introduces a v3.13.0 migration shadow, the pending_attestations LookupMap, four new gas knobs, and the VerificationAlreadyPending / VerifierNotConfigured errors.

Changes:

  • Async Dstack submission: yield/resume plumbing, pending-entry state, revert path on post-DCAP failure, refund on rejection/timeout.
  • New v3_13_0_state.rs migration snapshot so upgrades from the 3.13.0 release deserialize cleanly.
  • Errors: AttestationSubmissionError promoted to pub and lifted into Error as a top-level variant; TeeError gains VerificationAlreadyPending and VerifierNotConfigured.
  • Config: new verifier_tera_gas, resolve_verification_tera_gas, on_attestation_verified_tera_gas, fail_attestation_submission_tera_gas (plus DTO/InitConfig plumbing).
  • Method-name constants: verify_quote, resolve_verification, on_attestation_verified, fail_attestation_submission.
  • Removes the three inline unit tests that covered the old sync Dstack path; author confirms replacements land in stacked PR test(contract): sandbox coverage + stub verifier for async attestation #3715.
  • Regenerates ABI + borsh schema snapshots.

Reviewed changes

Per-file summary
File Description
crates/contract/src/lib.rs New async Dstack flow (submit_dstack_attestation, resolve_verification, verify_post_dcap_and_store, on_attestation_verified, fail_attestation_submission), pending-attestations map, deposit-refund helper, charge_attestation_storage refactor, migration path prepended for v3.13.0.
crates/contract/src/tee/pending_attestation.rs New PendingAttestation state (with TODO(#3720) to strip quote/collateral) and AttestationResult yield payload; JSON round-trip unit test.
crates/contract/src/tee/tee_state.rs Splits add_participant into verify_and_store_mock / verify_and_store_dstack, adds revert_dstack_store, expected_report_data, store_verified_attestation helpers; ParticipantInsertion::UpdatedExistingParticipant now carries the displaced entry.
crates/contract/src/v3_13_0_state.rs New migration shadow for the 3.13.0 release layout.
crates/contract/src/v3_12_0_state.rs Initializes pending_attestations in the v3.12.0 → current migration.
crates/contract/src/config.rs / dto_mapping.rs Four new gas knobs, default values, DTO mapping.
crates/contract/src/errors.rs New TeeError variants and AttestationSubmission top-level variant.
crates/contract/src/storage_keys.rs / tee.rs New PendingAttestations storage key + module wiring.
crates/contract/Cargo.toml / Cargo.lock Drop mpc-attestation/local-verify; add attestation and tee-verifier-interface; wire borsh-schema for ABI.
crates/mpc-attestation/src/attestation.rs Doc update: verify_locally no longer used by the contract.
crates/near-mpc-contract-interface/src/method_names.rs / types/config.rs New method-name constants and InitConfig/Config gas fields.
crates/contract/tests/inprocess/attestation_submission.rs Updates assertion to the new AttestationSubmission(TlsKeyOwnedByOtherAccount) variant.
crates/contract/tests/sandbox/{contract_configuration,upgrade_from_current_contract}.rs Populates the four new config fields.
crates/contract/tests/snapshots/abi__abi_has_not_changed.snap / src/snapshots/…borsh_schema…snap Regenerated ABI + borsh schema.
crates/test-utils/src/contract_types.rs dummy_config populates the four new fields.

Findings

Blocking issues from my prior review have been addressed in this iteration:

  • v3.13.0 state migration is now shadowed via crates/contract/src/v3_13_0_state.rs, with migrate() trying it before the v3.12.0 path. Field order in OldConfig matches the current Config on main (i.e. the deployed 3.13.0 layout).
  • PendingAttestation fields are pub(crate).
  • charge_attestation_storage now refunds the full attached deposit on the participant-refresh early return (refund_attestation_deposit(account_id, attached)), matching the docstring.
  • on_attestation_verified's success arm has the requested debug_assert!(!self.pending_attestations.contains_key(&account_id)) guarding the invariant that resolve_verification cleans up first.
  • Missing unit-level coverage for the new async paths is acknowledged and deferred to stacked PR test(contract): sandbox coverage + stub verifier for async attestation #3715.

Non-blocking (nits, follow-ups, suggestions):

  • crates/contract/src/lib.rs:135,845 vs lib.rs:2266,2299,2332NearToken::from_yoctonear(0) (new code) vs NearToken::from_near(0) (existing timeout callbacks). Same value, different spellings across files. Pick one to keep the codebase consistent.
  • crates/contract/src/lib.rs (async Dstack path) — the transient PendingAttestation entry (~200 blocks lifetime) is inserted using the contract's own storage staking; the caller's attached_deposit is stashed and refunded verbatim on timeout, but never accounted against the pending entry's storage cost. Bounded to one entry per account (via VerificationAlreadyPending) and self-clearing on timeout, so the blast radius is small — but with a verifier that takes the full yield window, a burst of Dstack submissions from distinct accounts could occupy contract-staked storage at zero cost to submitters. Worth a follow-up issue tracking either a per-pending storage charge or a minimum-deposit requirement for the Dstack path (probably alongside the Trim PendingAttestation to store only TcbInfo (drop quote/collateral) #3720 slimming that reduces the per-entry footprint).
  • crates/contract/src/lib.rs (submit_participant_info error contract) — this PR changes the shape of the error returned to callers: the "TLS key owned by other account" and "attestation failed verification" cases previously returned Error::InvalidParameters(InvalidTeeRemoteAttestation { reason: … }); they now return Error::AttestationSubmission(AttestationSubmissionError::…) with different Display output. attestation_submission.rs is updated in-tree, but any off-chain code / integration test / dashboard string-matching the old error text will silently break. If nothing off-chain matches these strings today, it's fine — please confirm, and add a note to the changelog either way.
  • crates/contract/src/lib.rs:820caller_is_participant = self.voter_account().is_ok() is now captured into PendingAttestation and consumed later in a callback receipt where voter_account() would no longer answer meaningfully. A caller who was a participant at submit time but is voted out during the yield window still gets the "free storage" refresh path. Impact is trivial (one small entry), but the semantics might drift silently — worth either a one-line comment on caller_is_participant in pending_attestation.rs calling out this design decision, or a dedicated caller_is_attested_participant() helper (still holds if you use it inside PendingAttestation::caller_is_participant).
  • crates/contract/src/lib.rs (resolve_verification) — the comment "MUST be the last host call: anything after could panic and roll back the state mutations above" is technically correct in intent, but reads oddly because the actual panic risk is the serde_json::to_vec(&attestation_result).expect(...) in the argument position, evaluated before promise_yield_resume. If that panics the receipt rolls back cleanly (pending stays, yield times out) — which is the safe fallback. Consider rephrasing the comment to describe the invariant it's protecting ("no state-mutating host calls may follow this point") rather than the panic mechanism.
  • crates/contract/src/lib.rs (submit_dstack_attestation) — a one-line pointer to tee_verifier_interface::verify_quote (or method_names::VERIFY_QUOTE) at the Promise::new(verifier_account_id).function_call(...) call site would make the implicit two-positional-args borsh wire contract discoverable without cross-crate hunting.
  • Gas defaults (crates/contract/src/config.rs:75-83) — DEFAULT_VERIFIER_TERA_GAS = 100, DEFAULT_RESOLVE_VERIFICATION_TERA_GAS = 60, DEFAULT_ON_ATTESTATION_VERIFIED_TERA_GAS = 10 are unbenchmarked estimates. Mirroring the cleanup_orphaned_node_migrations_tera_gas TODO with a TODO(#issue): benchmark once the stub verifier lands would make the follow-up work explicit and searchable.
  • Documentation alignment: I didn't find pre-existing prose docs describing the in-WASM DCAP path, but if any docs/design/* or module-level //! doc still describes contract-side dcap-qvl verification as the current design, flag/update it in the same PR per CLAUDE.md.

✅ Approved

pbeza added 4 commits July 1, 2026 17:09
- 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)
Comment on lines +2403 to +2406
let Some(pending) = self.pending_attestations.remove(&account_id) else {
log!(
"resolve_verification: no pending attestation for {account_id} (late response or already cleaned up); ignoring"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(doc): can you please align the design doc (#3715) here — its resolve_verification does .remove(..).expect(..), which would panic on exactly this late-response path (verifier answers after the ~200-block timeout already cleaned up). The graceful return here is the correct behavior; the doc should match.

Comment on lines +2509 to +2517
// Fail the submitter's transaction from a separate receipt so the
// cleanup above commits (a panic here would roll it back)
let promise = Promise::new(env::current_account_id()).function_call(
method_names::FAIL_ATTESTATION_SUBMISSION.to_string(),
borsh::to_vec(&reason).expect("borsh serialization of reason must succeed"),
NearToken::from_yoctonear(0),
Gas::from_tgas(self.config.fail_attestation_submission_tera_gas),
);
PromiseOrValue::Promise(promise.as_return())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(doc): can you please align the design doc (#3715) — it has on_attestation_verified -> Result<(), String> returning Err(reason), but the impl fails the caller's tx via a separate fail_attestation_submission receipt (with its own gas knob). Worth reflecting the actual mechanism so a future reader doesn't implement the return Err version.

Comment on lines +2425 to +2428
env::promise_yield_resume(
&pending.data_id,
serde_json::to_vec(&attestation_result)
.expect("json serialization of AttestationResult must succeed"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(doc): can you please align the design doc (#3715) — it resumes with borsh::to_vec(&FinalOutcome), but the impl uses JSON AttestationResult. Reconcile the name + serializer.

Comment on lines +852 to +865
fn submit_dstack_attestation(
&mut self,
node_id: NodeId,
dstack: DstackAttestation,
caller_is_participant: bool,
) -> Result<(), Error> {
let account_id = node_id.account_id.clone();

if self.pending_attestations.contains_key(&account_id) {
return Err(TeeError::VerificationAlreadyPending.into());
}

let Some(verifier_account_id) = self.tee_verifier_account_id.clone() else {
return Err(TeeError::VerifierNotConfigured.into());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(doc): can you please align the design doc (#3715) — a few names/fields drifted: finish_verifyverify_post_dcap_and_store, three → four gas knobs, PendingAttestation.caller_is_participant (absent from the doc's field list), and the guards here return VerificationAlreadyPending/VerifierNotConfigured rather than panicking as the doc describes.

@barakeinav1

Copy link
Copy Markdown
Contributor

Node-side follow-up (not this PR): the node polls get_participant_attestation TRANSACTION_TIMEOUT (10s) after submitting. The async round-trip (submit → verify_quoteresolve_verification stores) is ~3-4 blocks, so 10s usually covers the success/fast-fail path with no re-submit. But if the verifier round-trip ever creeps past 10s, the node re-submits into the in-flight entry and hits VerificationAlreadyPending. Once the verifier latency is benchmarked, we might consider bumping TRANSACTION_TIMEOUT a bit so the fast success/failure path never re-submits prematurely.

Comment on lines +888 to +907
self.enqueue_yield_request(
method_names::ON_ATTESTATION_VERIFIED,
serde_json::to_vec(&(&account_id,))
.expect("json serialization of account_id must succeed"),
Gas::from_tgas(self.config.on_attestation_verified_tera_gas),
|this, data_id| {
this.pending_attestations.insert(
account_id.clone(),
PendingAttestation {
dstack,
tls_public_key,
attached_deposit,
caller_is_participant,
data_id,
},
);
},
);

let attestation_storage_must_be_paid_by_caller =
is_new_attestation || caller_is_not_participant;
Ok(())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Claude for this one, so take it with a grain of salt — but it flagged something worth verifying.

Issue: submit_participant_info yields here (via enqueue_yield_requestenv::promise_return) yet returns Result<(), Error>. On the Ok(()) path, near-sdk's #[handle_result] codegen emits env::value_return(b"null") after the body's promise_return, and last-write-wins — so the yield gets clobbered and the caller's tx resolves immediately as success. The reject/timeout outcome computed later in on_attestation_verified then has nowhere to go. Note the other three yielding methods (sign, request_app_private_key, verify_foreign_transaction) all return () precisely to avoid this — a () method emits no value_return, so promise_return survives.

Net effect: a Dstack submission would always look like immediate success to the caller, regardless of the eventual verifier result.

Suggested fix (mirror sign): make submit_participant_info return () instead of Result<(), Error>, and env::panic_str on the early errors (VerifierNotConfigured, VerificationAlreadyPending, and the Mock-path InsufficientDeposit). A panic already fails the tx with a message — exactly what sign does ("It's important we fail here … This allows users to get the error message"). The on_attestation_verifiedfail_attestation_submission half already mirrors sign's fail_on_timeout, so no callback changes are needed.

On the removed tests: a unit test wouldn't have caught this — unit tests call the method directly and bypass the generated wasm entrypoint where value_return/promise_return actually live. This needs a sandbox test that submits a Dstack attestation and asserts the caller's tx parks and then fails on rejection/timeout. Worth confirming #3715 adds exactly that.

@barakeinav1 barakeinav1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on one blocker (flagged with Claude's help, so sanity-check me).

submit_participant_info yields but returns Result<(), Error>. On Ok(()), near-sdk emits value_return after the yield's promise_return and clobbers it — so the caller's tx returns immediate success and never sees a reject/timeout. The other yield methods (sign, etc.) return () to avoid exactly this. Fix: mirror sign — return () and panic on the early errors. And add a sandbox test; a unit test can't catch this.

Details: #3714 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire async TEE attestation verification through the verifier contract and drop dcap-qvl from mpc-contract

3 participants