Skip to content

Commit a11b816

Browse files
authored
test(contract): sandbox tests for the attestation flow against the real tee-verifier (#4165)
1 parent 547caa8 commit a11b816

25 files changed

Lines changed: 843 additions & 367 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,14 @@ jobs:
295295
if: github.ref == 'refs/heads/main'
296296
run: nix develop --command cargo near build reproducible-wasm --manifest-path crates/contract/Cargo.toml
297297

298+
- name: Build tee-verifier
299+
run: nix develop --command cargo make build-tee-verifier-optimized
300+
298301
- name: Build test-parallel-contract
299302
run: nix develop --command cargo make build-test-parallel-contract-optimized
300303

301304
- name: Build backup-cli
302-
run: nix develop --command cargo build -p backup-cli --release --locked
305+
run: nix develop --command cargo make build-backup-cli
303306

304307
- name: Run E2E tests
305308
run: nix develop --command cargo make e2e-tests-skip-build

Makefile.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ args = ["scripts/check-sandbox-image-version.sh"]
165165

166166
# These build tasks are the single source of truth for both local and CI builds.
167167
# CI's `mpc-e2e-tests` job invokes them via `cargo make`.
168-
# All three are skipped when `E2E_SKIP_BUILD` is set (used by `e2e-tests-skip-build`).
168+
# Each is skipped when `E2E_SKIP_BUILD` is set (used by `e2e-tests-skip-build`).
169169

170170
[tasks.build-mpc-node-network-hardship-simulation]
171171
description = "Build the mpc-node binary used by the E2E tests"
@@ -202,7 +202,8 @@ args = [
202202
]
203203

204204
[tasks.build-tee-verifier-optimized]
205-
description = "Build the tee-verifier WASM for localnet"
205+
description = "Build the tee-verifier WASM for localnet and the E2E tests"
206+
condition = { env_not_set = ["E2E_SKIP_BUILD"] }
206207
command = "cargo"
207208
args = [
208209
"near",
@@ -252,6 +253,7 @@ private = true
252253
dependencies = [
253254
"build-mpc-node-network-hardship-simulation",
254255
"build-mpc-contract-optimized",
256+
"build-tee-verifier-optimized",
255257
"build-test-parallel-contract-optimized",
256258
"build-backup-cli",
257259
]
@@ -272,8 +274,8 @@ args = [
272274
[tasks._run-e2e-logic.env]
273275
MPC_CONTRACT_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/mpc_contract/mpc_contract.wasm"
274276
MPC_PARALLEL_CONTRACT_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/test_parallel_contract/test_parallel_contract.wasm"
277+
MPC_TEE_VERIFIER_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/tee_verifier/tee_verifier.wasm"
275278

276-
# Build the mpc-node binary and both contract WASMs, then run the E2E tests.
277279
[tasks.e2e-tests]
278280
description = "Build required binaries and run the E2E tests"
279281
run_task = "_run-e2e-logic"

crates/contract/src/api/attestation.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -593,12 +593,8 @@ mod tests {
593593
&mut contract.tee_state,
594594
image_digest(),
595595
launcher_image_hash(),
596+
Some(launcher_compose_digest()),
596597
);
597-
// The fixture's launcher compose carries the key-export service, so its hash is not derivable.
598-
contract
599-
.tee_state
600-
.allowed_launcher_images
601-
.allow_compose_hash(&launcher_image_hash(), launcher_compose_digest());
602598

603599
// Storing a new entry consumes a grant, so stand in for the operator's prepayment.
604600
contract

crates/contract/src/tee/proposal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ impl AllowedLauncherImages {
491491
/// Test-only: allows one more compose hash for an already-allowed launcher. The attestation
492492
/// fixture is captured from a CVM whose launcher compose carries a key-export service, so
493493
/// [`get_docker_compose_hash`] cannot derive its hash.
494-
#[cfg(test)]
494+
#[cfg(any(test, feature = "test-utils"))]
495495
pub(crate) fn allow_compose_hash(
496496
&mut self,
497497
launcher_hash: &LauncherImageHash,

crates/contract/src/tee/tee_state.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,11 +1601,12 @@ mod tests {
16011601
set_block_timestamp(VALID_ATTESTATION_TIMESTAMP * 1_000_000_000);
16021602
let mut tee_state = TeeState::default();
16031603
assert_eq!(tee_state.stored_attestations.len(), 0);
1604-
whitelist_dstack_measurements(&mut tee_state, image_digest(), launcher_image_hash());
1605-
// The fixture's launcher compose carries the key-export service, so its hash is not derivable.
1606-
tee_state
1607-
.allowed_launcher_images
1608-
.allow_compose_hash(&launcher_image_hash(), launcher_compose_digest());
1604+
whitelist_dstack_measurements(
1605+
&mut tee_state,
1606+
image_digest(),
1607+
launcher_image_hash(),
1608+
Some(launcher_compose_digest()),
1609+
);
16091610
let node_id = NodeId {
16101611
account_id: "alice.near".parse().unwrap(),
16111612
tls_public_key: Ed25519PublicKey(p2p_tls_key()),

crates/contract/src/tee/test_utils.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
//! This module provides helper functions and types for testing TEE state,
44
//! attestation behavior, and general contract state management.
55
6+
use crate::MpcContract;
67
use crate::primitives::test_utils::{gen_account_id, gen_seed};
78
use crate::tee::{measurements::ContractExpectedMeasurements, tee_state::TeeState};
89
use mpc_attestation::attestation::default_measurements;
9-
use mpc_primitives::hash::{LauncherImageHash, NodeImageHash};
10+
use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash, NodeImageHash};
1011
use near_account_id::AccountId;
12+
use near_sdk::borsh::{self, BorshDeserialize};
1113
use near_sdk::{BlockHeight, NearToken, PublicKey, test_utils::VMContextBuilder, testing_env};
1214
use rand::Rng;
1315
use std::time::Duration;
@@ -104,14 +106,41 @@ pub fn set_block_timestamp(timestamp_nanos: u64) {
104106
);
105107
}
106108

109+
/// Fills the allowlists a Dstack submission is checked against: MPC image,
110+
/// launcher image, expected measurements, and optionally one compose hash.
111+
///
112+
/// The compose hash is injected directly because it is the one value no vote can
113+
/// authorize: [`get_docker_compose_hash`](crate::tee::proposal::get_docker_compose_hash)
114+
/// derives the accepted hashes from the voted launcher and MPC images through a
115+
/// compiled-in template, and the fixture's CVM ran a modified compose carrying a
116+
/// key-export service (the only way to get the signer key out of the CVM, which the
117+
/// quote's `report_data` binds and tests must sign with). Pass [`None`] to exercise
118+
/// the rejection path.
107119
pub fn whitelist_dstack_measurements(
108120
tee_state: &mut TeeState,
109121
image: NodeImageHash,
110122
launcher: LauncherImageHash,
123+
compose_hash: Option<LauncherDockerComposeHash>,
111124
) {
112125
tee_state.whitelist_tee_proposal(image, Duration::MAX);
113126
tee_state.add_launcher_image(launcher, Duration::MAX, Duration::MAX);
114127
for &measurements in default_measurements() {
115128
tee_state.add_measurement(ContractExpectedMeasurements::from(measurements));
116129
}
130+
if let Some(compose_hash) = compose_hash {
131+
tee_state
132+
.allowed_launcher_images
133+
.allow_compose_hash(&launcher, compose_hash);
134+
}
135+
}
136+
137+
pub fn whitelist_dstack_in_state(
138+
state: &[u8],
139+
image: NodeImageHash,
140+
launcher: LauncherImageHash,
141+
compose_hash: Option<LauncherDockerComposeHash>,
142+
) -> Vec<u8> {
143+
let mut contract = MpcContract::try_from_slice(state).expect("STATE deserializes");
144+
whitelist_dstack_measurements(&mut contract.tee_state, image, launcher, compose_hash);
145+
borsh::to_vec(&contract).expect("STATE serializes")
117146
}

crates/contract/tests/sandbox/tee.rs

Lines changed: 39 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ use crate::sandbox::{
1010
interface::IntoContractType,
1111
mpc_contract::{
1212
assert_running_return_participants, assert_running_return_threshold,
13-
get_participant_attestation, get_state, get_tee_accounts,
14-
prepay_and_submit_participant_info, prepay_attestation_grants, submit_participant_info,
15-
vote_add_launcher_hash, vote_for_hash,
13+
available_attestation_grants, get_config, get_participant_attestation, get_state,
14+
get_tee_accounts, prepay_and_submit_participant_info, prepay_attestation_grants,
15+
submit_participant_info, vote_add_launcher_hash, vote_for_hash,
1616
},
1717
resharing_utils::conclude_resharing,
1818
sign_utils::DomainResponseTest,
@@ -25,13 +25,22 @@ use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash, NodeIma
2525
use near_mpc_contract_interface::deposits::STORAGE_BYTE_COST_YOCTONEAR;
2626
use near_mpc_contract_interface::method_names;
2727
use near_mpc_contract_interface::types::{
28-
self as dtos, Attestation, Config, MockAttestation, Protocol,
28+
self as dtos, Attestation, MockAttestation, Protocol, VerifiedAttestation,
2929
};
3030
use near_workspaces::types::{KeyType, NearToken, SecretKey};
3131
use near_workspaces::{AccessKey, Account, Contract};
3232
use rand::SeedableRng;
3333
use test_utils::attestation::{image_digest, p2p_tls_key};
3434

35+
fn mock_expiring_at(expiry_timestamp_seconds: u64) -> MockAttestation {
36+
MockAttestation::WithConstraints {
37+
mpc_docker_image_hash: None,
38+
launcher_docker_compose_hash: None,
39+
expiry_timestamp_seconds: Some(expiry_timestamp_seconds),
40+
expected_measurements: None,
41+
}
42+
}
43+
3544
/// Tests the basic code hash voting mechanism including threshold behavior and vote stability.
3645
/// Validates that votes below threshold don't allow hashes, reaching threshold allows them,
3746
/// and additional votes don't change the allowed state or latest hash.
@@ -457,12 +466,7 @@ async fn clean_invalid_attestations__should_remove_expired_entries() -> Result<(
457466
let block_info = worker.view_block().await?;
458467
let expiry_timestamp_seconds =
459468
block_info.timestamp() / 1_000_000_000 + ATTESTATION_EXPIRY_SECONDS;
460-
let expiring_attestation = Attestation::Mock(MockAttestation::WithConstraints {
461-
mpc_docker_image_hash: None,
462-
launcher_docker_compose_hash: None,
463-
expiry_timestamp_seconds: Some(expiry_timestamp_seconds),
464-
expected_measurements: None,
465-
});
469+
let expiring_attestation = Attestation::Mock(mock_expiring_at(expiry_timestamp_seconds));
466470
let submit_result = prepay_and_submit_participant_info(
467471
stale_account,
468472
&contract,
@@ -595,10 +599,9 @@ async fn get_attestation_returns_none_when_tls_key_is_not_associated_with_an_att
595599

596600
assert!(validation_success);
597601

598-
let attestation_for_tls_key_2: Option<Attestation> =
599-
get_participant_attestation(&contract, &tls_key_2)
600-
.await
601-
.unwrap();
602+
let attestation_for_tls_key_2 = get_participant_attestation(&contract, &tls_key_2)
603+
.await
604+
.unwrap();
602605

603606
assert_eq!(attestation_for_tls_key_2, None);
604607
}
@@ -630,19 +633,10 @@ async fn get_attestation_returns_some_when_tls_key_associated_with_an_attestatio
630633
// as-is (a stored mock's expiry is `min(submitted, now + default window)`) and
631634
// the two attestations stay distinct.
632635
let now_seconds = worker.view_block().await.unwrap().timestamp() / 1_000_000_000;
633-
let participant_1_attestation = Attestation::Mock(MockAttestation::WithConstraints {
634-
mpc_docker_image_hash: None,
635-
launcher_docker_compose_hash: None,
636-
expiry_timestamp_seconds: Some(now_seconds + 1_000),
637-
expected_measurements: None,
638-
});
636+
let participant_1_attestation = Attestation::Mock(mock_expiring_at(now_seconds + 1_000));
639637

640-
let participant_2_attestation = Attestation::Mock(MockAttestation::WithConstraints {
641-
mpc_docker_image_hash: None,
642-
launcher_docker_compose_hash: None,
643-
expiry_timestamp_seconds: Some(now_seconds + 2_000),
644-
expected_measurements: None,
645-
});
638+
let participant_2_mock = mock_expiring_at(now_seconds + 2_000);
639+
let participant_2_attestation = Attestation::Mock(participant_2_mock.clone());
646640

647641
assert_ne!(
648642
participant_1_attestation, participant_2_attestation,
@@ -671,12 +665,14 @@ async fn get_attestation_returns_some_when_tls_key_associated_with_an_attestatio
671665
.is_success();
672666
assert!(validation_success, "Submitting attestation failed.");
673667

674-
let attestation_for_tls_key_2: Option<Attestation> =
675-
get_participant_attestation(&contract, &tls_key_2)
676-
.await
677-
.unwrap();
668+
let attestation_for_tls_key_2 = get_participant_attestation(&contract, &tls_key_2)
669+
.await
670+
.unwrap();
678671

679-
assert_eq!(attestation_for_tls_key_2, Some(participant_2_attestation));
672+
assert_eq!(
673+
attestation_for_tls_key_2,
674+
Some(VerifiedAttestation::Mock(participant_2_mock))
675+
);
680676
}
681677

682678
#[tokio::test]
@@ -698,19 +694,10 @@ async fn get_attestation_overwrites_when_same_tls_key_is_reused() {
698694
// as-is (a stored mock's expiry is `min(submitted, now + default window)`) and
699695
// the two attestations stay distinct.
700696
let now_seconds = worker.view_block().await.unwrap().timestamp() / 1_000_000_000;
701-
let first_attestation = Attestation::Mock(MockAttestation::WithConstraints {
702-
mpc_docker_image_hash: None,
703-
launcher_docker_compose_hash: None,
704-
expiry_timestamp_seconds: Some(now_seconds + 1_000),
705-
expected_measurements: None,
706-
});
697+
let first_attestation = Attestation::Mock(mock_expiring_at(now_seconds + 1_000));
707698

708-
let second_attestation = Attestation::Mock(MockAttestation::WithConstraints {
709-
mpc_docker_image_hash: None,
710-
launcher_docker_compose_hash: None,
711-
expiry_timestamp_seconds: Some(now_seconds + 2_000),
712-
expected_measurements: None,
713-
});
699+
let second_mock = mock_expiring_at(now_seconds + 2_000);
700+
let second_attestation = Attestation::Mock(second_mock.clone());
714701

715702
assert_ne!(
716703
first_attestation, second_attestation,
@@ -742,14 +729,13 @@ async fn get_attestation_overwrites_when_same_tls_key_is_reused() {
742729
assert!(validation_success, "Second attestation submission failed");
743730

744731
// Now the latest attestation should be returned
745-
let attestation_for_tls_key: Option<Attestation> =
746-
get_participant_attestation(&contract, &tls_key)
747-
.await
748-
.unwrap();
732+
let attestation_for_tls_key = get_participant_attestation(&contract, &tls_key)
733+
.await
734+
.unwrap();
749735

750736
assert_eq!(
751737
attestation_for_tls_key,
752-
Some(second_attestation),
738+
Some(VerifiedAttestation::Mock(second_mock)),
753739
"Expected the second attestation to overwrite the first for the same TLS key"
754740
);
755741
}
@@ -841,12 +827,7 @@ async fn test_verify_tee_expired_attestation_triggers_resharing() -> Result<()>
841827
.find(|node| node.account_id == *target_account.id())
842828
.expect("target participant not found");
843829

844-
let expiring_attestation = Attestation::Mock(MockAttestation::WithConstraints {
845-
mpc_docker_image_hash: None,
846-
launcher_docker_compose_hash: None,
847-
expiry_timestamp_seconds: Some(expiry_timestamp),
848-
expected_measurements: None,
849-
});
830+
let expiring_attestation = Attestation::Mock(mock_expiring_at(expiry_timestamp));
850831

851832
let submit_result = submit_participant_info(
852833
target_account,
@@ -963,12 +944,7 @@ async fn verify_tee__should_keep_participants_and_stop_signing_when_kickout_drop
963944
// Compute the expiry timestamp from the current block time.
964945
let block_info = worker.view_block().await?;
965946
let expiry_timestamp = block_info.timestamp() / 1_000_000_000 + ATTESTATION_EXPIRY_SECONDS;
966-
let expiring_attestation = Attestation::Mock(MockAttestation::WithConstraints {
967-
mpc_docker_image_hash: None,
968-
launcher_docker_compose_hash: None,
969-
expiry_timestamp_seconds: Some(expiry_timestamp),
970-
expected_measurements: None,
971-
});
947+
let expiring_attestation = Attestation::Mock(mock_expiring_at(expiry_timestamp));
972948

973949
// Submit an expiring attestation for every participant past the first `remaining_valid`.
974950
let internal_participants: Participants = (&initial_participants).into_contract_type();
@@ -1088,11 +1064,7 @@ async fn prepay_and_submit_a_constrained_mock__should_use_at_most_half_a_grant_f
10881064
});
10891065
let node = worker.dev_create_account().await?;
10901066
let tls_key = bogus_ed25519_public_key();
1091-
let config: Config = contract
1092-
.view(method_names::CONFIG)
1093-
.args_json(serde_json::json!({}))
1094-
.await?
1095-
.json()?;
1067+
let config = get_config(&contract).await?;
10961068
let before = contract.as_account().view_account().await?;
10971069

10981070
// When
@@ -1102,11 +1074,7 @@ async fn prepay_and_submit_a_constrained_mock__should_use_at_most_half_a_grant_f
11021074
assert!(submission.is_success(), "submission failed: {submission:?}");
11031075

11041076
// Then
1105-
let remaining: u32 = contract
1106-
.view(method_names::AVAILABLE_ATTESTATION_GRANTS)
1107-
.args_json(serde_json::json!({ "account_id": node.id() }))
1108-
.await?
1109-
.json()?;
1077+
let remaining = available_attestation_grants(&contract, node.id()).await?;
11101078
assert_eq!(remaining, GRANTS - 1, "the row must outlive the submission");
11111079

11121080
let after = contract.as_account().view_account().await?;
@@ -1163,11 +1131,7 @@ async fn submit_participant_info__should_store_a_new_entry_against_a_prepaid_gra
11631131
);
11641132
let stored = get_participant_attestation(&contract, &fresh_tls_key).await?;
11651133
assert!(stored.is_some(), "the entry should be stored on-chain");
1166-
let remaining: u32 = contract
1167-
.view(method_names::AVAILABLE_ATTESTATION_GRANTS)
1168-
.args_json(serde_json::json!({ "account_id": outsider.id() }))
1169-
.await?
1170-
.json()?;
1134+
let remaining = available_attestation_grants(&contract, outsider.id()).await?;
11711135
assert_eq!(remaining, 0, "storing a new entry should consume the grant");
11721136
Ok(())
11731137
}

0 commit comments

Comments
 (0)