44//! Each test deploys the `test-tee-verifier` stub, whose verify-quote returns a
55//! response the test picks instead of running real `dcap-qvl`, votes it in as the
66//! trusted verifier via [`vote_tee_verifier_change`], and then covers one branch
7- //! of the yield-resume flow:
7+ //! of the promise-chain flow.
88//!
9- //! - verifier not configured → submission rejected, nothing stored.
10- //! - [`StubResponse::Rejected`] → submission fails, deposit refunded, nothing stored.
11- //! - no-verdict (stub panics) → the ~200-block yield timeout cleans up.
12- //! - out-of-gas resolve → the receipt rolls back atomically and the same timeout
13- //! cleans up (no half-committed state).
9+ //! A Dstack submission spawns `verify_quote` on the trusted verifier with
10+ //! [`MpcContract::resolve_verification`] chained as its callback. There is no
11+ //! yield-resume and no timeout: [`resolve_verification`] settles every outcome
12+ //! synchronously within the same chain.
13+ //!
14+ //! - verifier not configured → the submit tx fails synchronously with
15+ //! [`TeeError::VerifierNotConfigured`], nothing stored.
16+ //! - [`StubResponse::Rejected`] → [`resolve_verification`] refunds the deposit and
17+ //! fires `fail_attestation_submission`, which panics in a separate receipt to
18+ //! fail the submitter's transaction; nothing stored.
19+ //! - stub panics (verifier unreachable) → the callback observes a failed promise,
20+ //! resolves to [`TeeError::VerifierUnavailable`], and fails the same way.
21+ //!
22+ //! On failure the top-level submit call still returns its chained promise, so the
23+ //! failure surfaces on the chain's receipt outcomes
24+ //! ([`ExecutionFinalResult::failures`]), not on the top-level tx result.
1425#![ allow( non_snake_case) ]
1526
1627use crate :: sandbox:: {
@@ -19,21 +30,19 @@ use crate::sandbox::{
1930 consts:: ALL_PROTOCOLS ,
2031 contract_build:: stub_tee_verifier_contract,
2132 mpc_contract:: {
22- get_participant_attestation, has_pending_attestation , submit_participant_info,
33+ get_participant_attestation, submit_participant_info,
2334 submit_participant_info_with_deposit, vote_tee_verifier_change,
2435 } ,
2536 } ,
2637} ;
2738use mpc_contract:: errors:: TeeError ;
2839use near_mpc_contract_interface:: types as dtos;
29- use near_workspaces:: { Account , Contract , Worker , network:: Sandbox , types:: NearToken } ;
40+ use near_workspaces:: {
41+ Account , Contract , Worker , network:: Sandbox , result:: ExecutionFinalResult , types:: NearToken ,
42+ } ;
3043use test_tee_verifier_types:: StubResponse ;
3144use test_utils:: attestation:: { mock_dto_dstack_attestation, p2p_tls_key, verified_report} ;
3245
33- /// Blocks to fast-forward past the ~200-block yield-resume timeout so the
34- /// runtime fires the yield-callback's timeout branch.
35- const YIELD_TIMEOUT_BLOCKS : u64 = 250 ;
36-
3746/// Deposit attached to a Dstack submission: covers storage on success, fully
3847/// refunded on failure.
3948const SUBMIT_DEPOSIT : NearToken = NearToken :: from_near ( 1 ) ;
@@ -72,9 +81,7 @@ async fn setup_with_stub(
7281 response : StubResponse ,
7382 init_config : Option < dtos:: InitConfig > ,
7483) -> ( Worker < Sandbox > , Contract , Account , NearToken ) {
75- let mut builder = SandboxTestSetup :: builder ( )
76- . with_protocols ( ALL_PROTOCOLS )
77- . with_sandbox_test_methods ( ) ;
84+ let mut builder = SandboxTestSetup :: builder ( ) . with_protocols ( ALL_PROTOCOLS ) ;
7885 if let Some ( init_config) = init_config {
7986 builder = builder. with_init_config ( init_config) ;
8087 }
@@ -91,33 +98,45 @@ async fn setup_with_stub(
9198 ( worker, contract, submitter, balance_before)
9299}
93100
94- async fn submit_dstack ( submitter : & Account , contract : & Contract ) {
95- let _ = submit_participant_info_with_deposit (
101+ async fn submit_dstack ( submitter : & Account , contract : & Contract ) -> ExecutionFinalResult {
102+ submit_participant_info_with_deposit (
96103 submitter,
97104 contract,
98105 & mock_dto_dstack_attestation ( ) ,
99106 & p2p_tls_key ( ) . into ( ) ,
100107 SUBMIT_DEPOSIT ,
101108 )
102109 . await
103- . unwrap ( ) ;
110+ . unwrap ( )
104111}
105112
106- /// Asserts a failed submission left no stored attestation, no pending entry, and
107- /// refunded the deposit.
108- async fn assert_submission_cleaned_up (
113+ /// Asserts a Dstack submission failed on the chain and left no committed state:
114+ /// the failure surfaces on a receipt (`fail_attestation_submission` panics in its
115+ /// own receipt), carries `expected_error`, nothing is stored, and the deposit is
116+ /// refunded.
117+ async fn assert_submission_failed_cleanly (
118+ result : & ExecutionFinalResult ,
109119 contract : & Contract ,
110120 submitter : & Account ,
111121 balance_before : NearToken ,
122+ expected_error : & TeeError ,
112123) {
124+ let failures = result. failures ( ) ;
125+ assert ! (
126+ !failures. is_empty( ) ,
127+ "expected the promise chain to fail on a receipt, got: {result:#?}"
128+ ) ;
129+ let rendered = format ! ( "{failures:?}" ) ;
130+ let expected = expected_error. to_string ( ) ;
131+ assert ! (
132+ rendered. contains( & expected) ,
133+ "expected a receipt failure containing {expected:?}, got: {rendered}"
134+ ) ;
135+
113136 let stored = get_participant_attestation ( contract, & p2p_tls_key ( ) . into ( ) )
114137 . await
115138 . unwrap ( ) ;
116139 assert ! ( stored. is_none( ) , "nothing should be stored on failure" ) ;
117- assert ! (
118- !has_pending_attestation( contract, submitter. id( ) ) . await . unwrap( ) ,
119- "the pending entry must be cleaned up"
120- ) ;
121140 assert_deposit_refunded ( submitter, balance_before) . await ;
122141}
123142
@@ -143,8 +162,8 @@ async fn submit_participant_info__should_reject_dstack_when_verifier_not_configu
143162 . await
144163 . unwrap ( ) ;
145164
146- // Then: it fails synchronously (before any yield ), so the error is on the tx
147- // result.
165+ // Then: it fails synchronously (before any cross-contract call ), so the error
166+ // is on the top-level tx result, not a later receipt .
148167 let err = result
149168 . into_result ( )
150169 . expect_err ( "Dstack submit must fail when no verifier is configured" )
@@ -170,58 +189,121 @@ async fn submit_participant_info__should_refund_and_store_nothing_on_verifier_re
170189 setup_with_stub ( StubResponse :: Rejected ( "test rejection" . to_string ( ) ) , None ) . await ;
171190
172191 // When: a Dstack attestation is submitted.
173- submit_dstack ( & submitter, & contract) . await ;
192+ let result = submit_dstack ( & submitter, & contract) . await ;
174193
175- // Then: the submission is cleaned up. The rejection resolves in the verifier's
176- // response receipt, so the outcome is observable in state, not the tx result.
177- assert_submission_cleaned_up ( & contract, & submitter, balance_before) . await ;
194+ // Then: resolve_verification refunds and fails the submission in a separate
195+ // receipt; the failure is on the chain, not the top-level tx result. The
196+ // stub wraps the reason in `VerifierError::DcapVerification`, whose Display
197+ // prefixes "dcap verification failed: ".
198+ assert_submission_failed_cleanly (
199+ & result,
200+ & contract,
201+ & submitter,
202+ balance_before,
203+ & TeeError :: QuoteRejected {
204+ reason : "dcap verification failed: test rejection" . to_string ( ) ,
205+ } ,
206+ )
207+ . await ;
178208}
179209
180210#[ tokio:: test]
181- async fn submit_participant_info__should_clean_up_on_verifier_crash ( ) {
182- // Given: a verifier that panics, so no resume lands .
183- let ( worker , contract, submitter, balance_before) =
211+ async fn submit_participant_info__should_fail_and_store_nothing_on_verifier_crash ( ) {
212+ // Given: a verifier that panics, so the verify_quote promise fails .
213+ let ( _worker , contract, submitter, balance_before) =
184214 setup_with_stub ( StubResponse :: Panic , None ) . await ;
185215
186- // When: a submission times out (no verdict within the yield window).
187- submit_dstack ( & submitter, & contract) . await ;
188- worker. fast_forward ( YIELD_TIMEOUT_BLOCKS ) . await . unwrap ( ) ;
216+ // When: a Dstack attestation is submitted.
217+ let result = submit_dstack ( & submitter, & contract) . await ;
189218
190- // Then: the timeout cleans up. Guards the regression where cleanup was rolled
191- // back by a panic in the same receipt, leaking the entry and wedging the account.
192- assert_submission_cleaned_up ( & contract, & submitter, balance_before) . await ;
219+ // Then: the callback sees a failed promise, resolves to VerifierUnavailable,
220+ // refunds, and fails the submission in a separate receipt. No timeout: the
221+ // outcome settles synchronously within the same chain.
222+ assert_submission_failed_cleanly (
223+ & result,
224+ & contract,
225+ & submitter,
226+ balance_before,
227+ & TeeError :: VerifierUnavailable ,
228+ )
229+ . await ;
193230}
194231
195- // TODO(#3730 ): un-ignore once the fixture allowlist setup lands. To OOG,
196- // `resolve_verification` must reach the heavy RTMR3 replay, which needs the
197- // post-DCAP allowlist checks to pass first (fixture image/launcher hashes and
198- // measurements voted in, submitter using the fixture keys) . With an empty
199- // allowlist the check fails fast and `resolve_verification` completes at 1 TGas,
200- // so this would re-test the rejection path .
201- #[ ignore = "needs fixture allowlist setup to reach the gas-heavy post-DCAP path ; tracked in #3730 " ]
232+ // TODO(#3738 ): un-ignore once the fixture allowlist setup lands. A Verified
233+ // verdict routes through `verify_post_dcap_and_store`, whose allowlist checks
234+ // (fixture image/launcher hashes and measurements voted in, submitter using the
235+ // fixture keys) must pass before the attestation is stored . With an empty
236+ // allowlist the post-DCAP check fails and the submission is rejected instead of
237+ // stored, so the happy path cannot be exercised here yet .
238+ #[ ignore = "needs fixture allowlist setup to pass the post-DCAP checks ; tracked in #3738 " ]
202239#[ tokio:: test]
203- async fn submit_participant_info__should_clean_up_when_resolve_verification_runs_out_of_gas ( ) {
240+ async fn submit_participant_info__should_store_attestation_on_verified_quote ( ) {
241+ // Given: a verifier that returns the report the real verifier would produce
242+ // for the fixture quote.
243+ let ( _worker, contract, submitter, balance_before) =
244+ setup_with_stub ( StubResponse :: Verified ( verified_report ( ) ) , None ) . await ;
245+
246+ // When: a Dstack attestation is submitted.
247+ let result = submit_dstack ( & submitter, & contract) . await ;
248+
249+ // Then: the chain succeeds and the attestation is stored; storage is charged
250+ // and the excess deposit refunded (net spend is storage + gas, well under the
251+ // full deposit).
252+ assert ! (
253+ result. failures( ) . is_empty( ) ,
254+ "the verified submission chain must succeed, got: {result:#?}"
255+ ) ;
256+ let stored = get_participant_attestation ( & contract, & p2p_tls_key ( ) . into ( ) )
257+ . await
258+ . unwrap ( ) ;
259+ assert ! ( stored. is_some( ) , "a verified attestation must be stored" ) ;
260+ let balance_after = submitter. view_account ( ) . await . unwrap ( ) . balance ;
261+ assert ! (
262+ balance_after < balance_before,
263+ "storage must be charged from the attached deposit"
264+ ) ;
265+ }
266+
267+ // TODO(#3738): un-ignore once the fixture allowlist setup lands. To OOG,
268+ // `resolve_verification` must reach the heavy RTMR3 replay in the post-DCAP
269+ // checks, which needs the allowlist populated and the submitter using the fixture
270+ // keys. With an empty allowlist the post-DCAP check fails fast and
271+ // `resolve_verification` completes well under 1 TGas, re-testing the rejection
272+ // path instead. Under the promise-chain model an OOG rolls the whole callback
273+ // receipt back atomically: nothing is stored, the runtime refunds the attached
274+ // deposit to the predecessor, and `fail_attestation_submission` never fires, so
275+ // the chain still surfaces a failed receipt. No timeout is involved.
276+ #[ ignore = "needs fixture allowlist setup to reach the gas-heavy post-DCAP path; tracked in #3738" ]
277+ #[ tokio:: test]
278+ async fn submit_participant_info__should_fail_and_store_nothing_when_resolve_verification_runs_out_of_gas ( )
279+ {
204280 // Given: a Verified stub and a resolve gas budget too small for the post-DCAP
205- // work, so that branch OOGs and rolls back. (Rejected is too light to OOG.)
281+ // work, so that callback OOGs and rolls back atomically.
206282 let init_config = dtos:: InitConfig {
207283 resolve_verification_tera_gas : Some ( 1 ) ,
208284 ..Default :: default ( )
209285 } ;
210- let ( worker , contract, submitter, balance_before) =
286+ let ( _worker , contract, submitter, balance_before) =
211287 setup_with_stub ( StubResponse :: Verified ( verified_report ( ) ) , Some ( init_config) ) . await ;
212288
213- // When: a submission is made; resolve rolls back rather than resuming .
214- submit_dstack ( & submitter, & contract) . await ;
289+ // When: a Dstack attestation is submitted .
290+ let result = submit_dstack ( & submitter, & contract) . await ;
215291
216- // Then: unlike the rejection path, the entry is still pending before the
217- // timeout; the timeout then cleans up, proving an atomic rollback of a partial
218- // resolve receipt cannot wedge the account.
292+ // Then: the callback receipt fails wholesale, nothing is stored, and the
293+ // runtime refunds the attached deposit. Proves an OOG in resolve cannot commit
294+ // partial state.
295+ assert ! (
296+ !result. failures( ) . is_empty( ) ,
297+ "an OOG resolve_verification must fail the chain, got: {result:#?}"
298+ ) ;
299+ let stored = get_participant_attestation ( & contract, & p2p_tls_key ( ) . into ( ) )
300+ . await
301+ . unwrap ( ) ;
219302 assert ! (
220- has_pending_attestation ( & contract , submitter . id ( ) ) . await . unwrap ( ) ,
221- "pending entry must survive an OOG resolve_verification (cleanup is left to the timeout) "
303+ stored . is_none ( ) ,
304+ "nothing should be stored on an OOG resolve "
222305 ) ;
223- worker. fast_forward ( YIELD_TIMEOUT_BLOCKS ) . await . unwrap ( ) ;
224- assert_submission_cleaned_up ( & contract, & submitter, balance_before) . await ;
306+ assert_deposit_refunded ( & submitter, balance_before) . await ;
225307}
226308
227309/// Asserts the full 1 NEAR storage deposit was returned: the net spend is only
0 commit comments