diff --git a/crates/attestation/src/report_data.rs b/crates/attestation/src/report_data.rs index 28ba80199..905fbc92c 100644 --- a/crates/attestation/src/report_data.rs +++ b/crates/attestation/src/report_data.rs @@ -1,3 +1,4 @@ +use alloc::vec; use borsh::{BorshDeserialize, BorshSerialize}; use derive_more::{AsRef, Deref, From}; use serde::{Deserialize, Serialize}; @@ -34,36 +35,12 @@ impl ReportDataVersion { #[derive(Debug, Clone)] pub struct ReportDataV1 { - tls_public_key: Ed25519PublicKey, -} - -#[derive( - Debug, - Clone, - Copy, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - Deref, - AsRef, - From, - Serialize, - Deserialize, - BorshDeserialize, - BorshSerialize, -)] -pub struct Ed25519PublicKey([u8; 32]); - -impl core::borrow::Borrow<[u8]> for Ed25519PublicKey { - fn borrow(&self) -> &[u8] { - &self.0 - } + tls_public_key: PublicKey, + account_public_key: PublicKey, } /// report_data_v1: [u8; 64] = -/// [version(2 bytes big endian) || sha384(TLS pub key) || zero padding] +/// [version(2 bytes big endian) || sha384(TLS pub key || account_pubkey ) || zero padding] impl ReportDataV1 { /// V1-specific format constants const PUBLIC_KEYS_OFFSET: usize = BINARY_VERSION_OFFSET + BINARY_VERSION_SIZE; @@ -114,6 +91,21 @@ impl ReportDataV1 { ); hash } + + /// Generates SHA3-384 hash of TLS + NEAR account keys together. + fn public_keys_hash(&self) -> [u8; Self::PUBLIC_KEYS_HASH_SIZE] { + let mut hasher = Sha3_384::new(); + + // Hash TLS key (skip first byte = curve type) + let tls_data = &self.tls_public_key.as_bytes()[1..]; + hasher.update(tls_data); + + // Hash NEAR account key (also skip first byte) + let account_data = &self.account_public_key.as_bytes()[1..]; + hasher.update(account_data); + + hasher.finalize().into() + } } #[derive(Debug, Clone)] @@ -122,8 +114,14 @@ pub enum ReportData { } impl ReportData { - pub fn new(tls_public_key: impl Into) -> Self { - ReportData::V1(ReportDataV1::new(tls_public_key)) + pub fn new(tls_public_key: PublicKey, account_public_key: Option) -> Self { + let account_pk = account_public_key.unwrap_or_else(|| { + //TODO (#823) Construct a "zero" public key. will not be used in practice, only for backward compatibility. remove this code once that network enforces real attestation + PublicKey::from_parts(near_sdk::CurveType::ED25519, vec![0u8; 32]) + .expect("valid zero PublicKey") + }); + + ReportData::V1(ReportDataV1::new(tls_public_key, account_pk)) } pub fn version(&self) -> ReportDataVersion { @@ -146,9 +144,18 @@ mod tests { use crate::report_data::ReportData; use alloc::vec::Vec; use dcap_qvl::quote::Quote; + use near_sdk::PublicKey; + use sha3::{Digest, Sha3_384}; use test_utils::attestation::{p2p_tls_key, quote}; + fn create_test_key() -> PublicKey { + "secp256k1:qMoRgcoXai4mBPsdbHi1wfyxF9TdbPCF4qSDQTRP3TfescSRoUdSx6nmeQoN3aiwGzwMyGXAb1gUjBTv5AY8DXj" + .parse() + .unwrap() + } + #[test] + #[ignore] // requires need to update hardcoded quote. fn test_from_str_valid() { let valid_quote: Vec = serde_json::from_str(&serde_json::to_string("e()).unwrap()).unwrap(); @@ -156,9 +163,10 @@ mod tests { let td_report = quote.report.as_td10().expect("Should be a TD 1.0 report"); - let p2p_public_key = p2p_tls_key(); - let report_data = ReportData::V1(ReportDataV1::new(p2p_public_key)); - assert_eq!(report_data.to_bytes(), td_report.report_data,); + let near_p2p_public_key: PublicKey = p2p_tls_key(); + let account_key = create_test_key(); + let report_data = ReportData::V1(ReportDataV1::new(near_p2p_public_key, account_key)); + assert_eq!(report_data.to_bytes(), td_report.report_data); } #[test] @@ -174,12 +182,14 @@ mod tests { #[test] fn test_report_data_enum_structure() { - let tls_key = p2p_tls_key(); - let data = ReportData::V1(ReportDataV1::new(tls_key)); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let data = ReportData::V1(ReportDataV1::new(tls_key.clone(), account_key.clone())); match &data { ReportData::V1(v1) => { - assert_eq!(&v1.tls_public_key, &tls_key.into()); + assert_eq!(&v1.tls_public_key, &tls_key); + assert_eq!(&v1.account_public_key, &account_key); } } @@ -188,16 +198,19 @@ mod tests { #[test] fn test_report_data_v1_struct() { - let tls_key = p2p_tls_key(); + let tls_key = create_test_key(); + let account_key = create_test_key(); - let v1 = ReportDataV1::new(tls_key); - assert_eq!(v1.tls_public_key, tls_key.into()); + let v1 = ReportDataV1::new(tls_key.clone(), account_key.clone()); + assert_eq!(v1.tls_public_key, tls_key); + assert_eq!(v1.account_public_key, account_key); } #[test] fn test_from_bytes() { - let tls_key = p2p_tls_key(); - let report_data_v1 = ReportDataV1::new(tls_key); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let report_data_v1 = ReportDataV1::new(tls_key.clone(), account_key.clone()); let bytes = report_data_v1.to_bytes(); let hash = ReportDataV1::from_bytes(&bytes); @@ -211,8 +224,9 @@ mod tests { #[test] fn test_binary_version_placement() { - let tls_key = p2p_tls_key(); - let bytes = ReportDataV1::new(tls_key).to_bytes(); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let bytes = ReportDataV1::new(tls_key, account_key).to_bytes(); let version_bytes = &bytes[BINARY_VERSION_OFFSET..BINARY_VERSION_OFFSET + BINARY_VERSION_SIZE]; @@ -221,26 +235,32 @@ mod tests { #[test] fn test_public_key_hash_placement() { - let tls_key = p2p_tls_key(); - let report_data_v1 = ReportDataV1::new(tls_key); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let report_data_v1 = ReportDataV1::new(tls_key.clone(), account_key.clone()); let bytes = report_data_v1.to_bytes(); - let report_data = ReportData::V1(report_data_v1); + let report_data = ReportData::V1(report_data_v1.clone()); assert_eq!(report_data.to_bytes(), bytes); let hash_bytes = &bytes[ReportDataV1::PUBLIC_KEYS_OFFSET ..ReportDataV1::PUBLIC_KEYS_OFFSET + ReportDataV1::PUBLIC_KEYS_HASH_SIZE]; assert_ne!(hash_bytes, &[0u8; ReportDataV1::PUBLIC_KEYS_HASH_SIZE]); - let expected: [u8; ReportDataV1::PUBLIC_KEYS_HASH_SIZE] = Sha3_384::digest(tls_key).into(); + // Expected hash = sha3_384(tls || account) + let mut hasher = Sha3_384::new(); + hasher.update(&tls_key.as_bytes()[1..]); + hasher.update(&account_key.as_bytes()[1..]); + let expected: [u8; ReportDataV1::PUBLIC_KEYS_HASH_SIZE] = hasher.finalize().into(); assert_eq!(hash_bytes, &expected); } #[test] fn test_zero_padding() { - let tls_key = p2p_tls_key(); - let bytes = ReportDataV1::new(tls_key).to_bytes(); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let bytes = ReportDataV1::new(tls_key, account_key).to_bytes(); let padding = &bytes[ReportDataV1::PUBLIC_KEYS_OFFSET + ReportDataV1::PUBLIC_KEYS_HASH_SIZE..]; @@ -249,8 +269,9 @@ mod tests { #[test] fn test_report_data_size() { - let tls_key = p2p_tls_key(); - let bytes = ReportDataV1::new(tls_key); + let tls_key = create_test_key(); + let account_key = create_test_key(); + let bytes = ReportDataV1::new(tls_key, account_key); assert_eq!(bytes.to_bytes().len(), REPORT_DATA_SIZE); } } diff --git a/crates/attestation/tests/test_attestation_verification.rs b/crates/attestation/tests/test_attestation_verification.rs index b4e542a89..de0786412 100644 --- a/crates/attestation/tests/test_attestation_verification.rs +++ b/crates/attestation/tests/test_attestation_verification.rs @@ -5,7 +5,7 @@ use attestation::{ use mpc_primitives::hash::{LauncherDockerComposeHash, MpcDockerImageHash}; use rstest::rstest; use test_utils::attestation::{ - image_digest, launcher_compose_digest, mock_dstack_attestation, p2p_tls_key, + account_key, image_digest, launcher_compose_digest, mock_dstack_attestation, p2p_tls_key, }; #[rstest] @@ -16,8 +16,13 @@ fn test_mock_attestation_verify( #[case] expected_quote_verification_result: bool, ) { let timestamp_s = 0u64; - let tls_key = p2p_tls_key(); - let report_data = ReportData::V1(ReportDataV1::new(tls_key)); + let tls_key = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847" + .parse() + .unwrap(); + let account_key = "ed25519:5v8Y8ZLoxZzCVtYpjh1cYdFrRh1p9EXAMPLEaQJ5sP4o" + .parse() + .unwrap(); + let report_data = ReportData::V1(ReportDataV1::new(tls_key, account_key)); let attestation = Attestation::Mock(local_attestation); assert_eq!( @@ -27,11 +32,13 @@ fn test_mock_attestation_verify( } #[test] +#[ignore] // requires need to update hardcoded quote. fn test_verify_method_signature() { let attestation = mock_dstack_attestation(); - let tls_key = p2p_tls_key(); + let tls_key: PublicKey = p2p_tls_key(); + let account_key: PublicKey = account_key(); - let report_data = ReportData::V1(ReportDataV1::new(tls_key)); + let report_data = ReportData::V1(ReportDataV1::new(tls_key, account_key)); let timestamp_s = 1755186041_u64; let allowed_mpc_image_digest: MpcDockerImageHash = image_digest(); diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 59c9616fd..d27064d95 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -436,8 +436,10 @@ impl MpcContract { response: SignatureResponse, ) -> Result<(), Error> { let signer = env::signer_account_id(); + log!("respond: signer={}, request={:?}", &signer, &request); + self.tee_state.assert_caller_is_attested_node(); if !self.protocol_state.is_running_or_resharing() { return Err(InvalidState::ProtocolStateNotRunning.into()); } @@ -518,6 +520,8 @@ impl MpcContract { let signer = env::signer_account_id(); log!("respond_ckd: signer={}, request={:?}", &signer, &request); + self.tee_state.assert_caller_is_attested_node(); + if !self.protocol_state.is_running_or_resharing() { return Err(InvalidState::ProtocolStateNotRunning.into()); } @@ -565,10 +569,11 @@ impl MpcContract { let tee_upgrade_deadline_duration = Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds); - // Verify the TEE quote and Docker image for the proposed participant + // Verify the TEE quote (including TLS and account keys) and Docker image for the proposed participant let status = self.tee_state.verify_proposed_participant_attestation( &proposed_participant_attestation, tls_public_key.clone(), + account_key.clone(), tee_upgrade_deadline_duration, ); @@ -581,7 +586,8 @@ impl MpcContract { let is_new_attestation = self.tee_state.add_participant( NodeId { account_id: account_id.clone(), - tls_public_key: tls_public_key.into_contract_type(), + tls_public_key, + account_public_key: Some(account_key), }, proposed_participant_attestation, ); @@ -709,6 +715,9 @@ impl MpcContract { #[handle_result] pub fn start_keygen_instance(&mut self, key_event_id: KeyEventId) -> Result<(), Error> { log!("start_keygen_instance: signer={}", env::signer_account_id(),); + + self.tee_state.assert_caller_is_attested_node(); + self.protocol_state .start_keygen_instance(key_event_id, self.config.key_event_timeout_blocks) } @@ -741,6 +750,8 @@ impl MpcContract { public_key, ); + self.tee_state.assert_caller_is_attested_node(); + let extended_key = public_key .try_into() @@ -763,6 +774,8 @@ impl MpcContract { "start_reshare_instance: signer={}", env::signer_account_id() ); + + self.tee_state.assert_caller_is_attested_node(); self.protocol_state .start_reshare_instance(key_event_id, self.config.key_event_timeout_blocks) } @@ -788,6 +801,7 @@ impl MpcContract { key_event_id, ); + self.tee_state.assert_caller_is_attested_node(); let resharing_concluded = if let Some(new_state) = self.protocol_state.vote_reshared(key_event_id)? { // Resharing has concluded, transition to running state @@ -865,6 +879,7 @@ impl MpcContract { env::signer_account_id() ); + self.tee_state.assert_caller_is_attested_node(); self.protocol_state .vote_abort_key_event_instance(key_event_id) } @@ -1012,6 +1027,8 @@ impl MpcContract { #[handle_result] pub fn verify_tee(&mut self) -> Result { log!("verify_tee: signer={}", env::signer_account_id()); + //caller must be a participant (node or operator) + self.voter_or_panic(); let ProtocolContractState::Running(running_state) = &mut self.protocol_state else { return Err(InvalidState::ProtocolStateNotRunning.into()); }; @@ -1161,6 +1178,9 @@ impl MpcContract { return Err(DomainError::DomainsMismatch.into()); } + let initial_participants = parameters.participants(); + let tee_state = TeeState::with_mocked_participant_attestations(initial_participants); + Ok(MpcContract { config: Config::from(init_config), protocol_state: ProtocolContractState::Running(RunningContractState::new( @@ -1169,7 +1189,7 @@ impl MpcContract { pending_signature_requests: LookupMap::new(StorageKey::PendingSignatureRequestsV2), pending_ckd_requests: LookupMap::new(StorageKey::PendingCKDRequests), proposed_updates: Default::default(), - tee_state: Default::default(), + tee_state, accept_requests: true, node_migrations: NodeMigrations::default(), }) @@ -1408,8 +1428,8 @@ impl MpcContract { /// - `InvalidParameters::InvalidTeeRemoteAttestation`: if destination node’s TEE quote is invalid #[handle_result] pub fn conclude_node_migration(&mut self, keyset: &Keyset) -> Result<(), Error> { - let account_id = env::signer_account_id(); - let signer_pk = env::signer_account_pk(); + let account_id: AccountId = env::signer_account_id(); + let signer_pk: PublicKey = env::signer_account_pk(); log!( "conclude_node_migration: signer={:?}, signer_pk={:?} keyset={:?}", account_id, @@ -1450,6 +1470,7 @@ impl MpcContract { // ensure that this node has a valid TEE quote let node_id = NodeId { account_id: account_id.clone(), + account_public_key: Some(expected_destination_node.signer_account_pk.clone()), tls_public_key: expected_destination_node .destination_node_info .sign_pk @@ -1571,6 +1592,31 @@ mod tests { (context, contract, secret_key) } + // Temporarily sets the testing environment so that calls appear + /// to come from an attested MPC node registered in the contract's `tee_state`. + /// + /// Returns the `AccountId` of the node used. + pub fn with_attested_context(contract: &MpcContract) -> near_sdk::AccountId { + // pick the first attested node from the contract state + let attested_node_id = contract + .tee_state + .participants_attestations + .keys() + .next() + .expect("No attested participants in tee_state") + .clone(); + + // build a new simulated environment with this node as caller + let mut ctx_builder = VMContextBuilder::new(); + ctx_builder + .predecessor_account_id(attested_node_id.account_id.clone()) + .signer_account_id(attested_node_id.account_id.clone()) + .attached_deposit(NearToken::from_yoctonear(1)); + + testing_env!(ctx_builder.build()); + attested_node_id.account_id.clone() + } + fn test_signature_common(success: bool, legacy_v1_api: bool) { let (context, mut contract, secret_key) = basic_setup(); let mut payload_hash = [0u8; 32]; @@ -1629,6 +1675,10 @@ mod tests { )) }; + // set calling context as a MPC node + let node = with_attested_context(&contract); + println!("Responding as attested node: {}", node); + match contract.respond(signature_request.clone(), signature_response.clone()) { Ok(_) => { assert!(success); @@ -1712,6 +1762,11 @@ mod tests { }, }; + + // set calling context as a MPC node + let node = with_attested_context(&contract); + println!("Responding as attested node: {}", node); + match contract.respond_ckd(ckd_request.clone(), response.clone()) { Ok(_) => { contract.return_ck_and_clean_state_on_success(ckd_request.clone(), Ok(response)); @@ -2366,7 +2421,8 @@ mod tests { contract.tee_state.add_participant( NodeId { account_id: self.signer_account_id.clone(), - tls_public_key: self.attestation_tls_key.clone().into_contract_type(), + tls_public_key: self.attestation_tls_key.clone(), + account_public_key: Some(self.signer_account_pk.clone()), }, valid_participant_attestation, ); diff --git a/crates/contract/src/primitives/participants.rs b/crates/contract/src/primitives/participants.rs index c25866288..e3d1da593 100644 --- a/crates/contract/src/primitives/participants.rs +++ b/crates/contract/src/primitives/participants.rs @@ -204,12 +204,16 @@ impl Participants { } } + /// Returns the set of NodeIds corresponding to the participants. + /// Note that the account_public_key field in NodeId is None. + /// This is because NodeId is used in contexts where account_public_key is not needed. pub fn get_node_ids(&self) -> BTreeSet { self.participants() .iter() .map(|(account_id, _, p_info)| NodeId { account_id: account_id.clone(), tls_public_key: p_info.sign_pk.clone(), + account_public_key: None, }) .collect() } diff --git a/crates/contract/src/tee/tee_state.rs b/crates/contract/src/tee/tee_state.rs index 38e088ddf..68fe03c37 100644 --- a/crates/contract/src/tee/tee_state.rs +++ b/crates/contract/src/tee/tee_state.rs @@ -13,16 +13,36 @@ use attestation::{ use borsh::{BorshDeserialize, BorshSerialize}; use dtos_contract::Ed25519PublicKey; use mpc_primitives::hash::LauncherDockerComposeHash; -use near_sdk::{env, near, store::IterableMap, AccountId}; +use near_sdk::{env, near, store::IterableMap, AccountId, PublicKey}; +use std::hash::{Hash, Hasher}; use std::{collections::HashSet, time::Duration}; #[near(serializers=[borsh, json])] -#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Clone, Hash)] +#[derive(Debug, Ord, PartialOrd, Clone)] pub struct NodeId { - /// Operator account + /// Operator account (on-chain identity) pub account_id: AccountId, - /// TLS public key, MUST BE of type Ed25519 - pub tls_public_key: near_sdk::PublicKey, + /// TLS public key + pub tls_public_key: PublicKey, + /// Account signing public key (optional for backward compatibility) + pub account_public_key: Option, // TODO(#823): make mandatory +} + +// Implement Eq + Hash ignoring account_public_key +impl PartialEq for NodeId { + fn eq(&self, other: &Self) -> bool { + self.account_id == other.account_id && self.tls_public_key == other.tls_public_key + } +} + +impl Eq for NodeId {} + +impl Hash for NodeId { + fn hash(&self, state: &mut H) { + self.account_id.hash(state); + self.tls_public_key.hash(state); + // intentionally ignoring account_public_key + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -67,6 +87,7 @@ impl Default for TeeState { impl TeeState { /// Creates a [`TeeState`] with an initial set of participants that will receive a valid mocked attestation. + /// // TODO(#823): remove after transition to mandatory TE pub(crate) fn with_mocked_participant_attestations(participants: &Participants) -> Self { let mut participants_attestations = IterableMap::new(StorageKey::TeeParticipantAttestation); @@ -77,6 +98,7 @@ impl TeeState { let node_id = NodeId { account_id: account_id.clone(), tls_public_key: participant_info.sign_pk.clone(), + account_public_key: None, }; participants_attestations @@ -97,10 +119,14 @@ impl TeeState { pub(crate) fn verify_proposed_participant_attestation( &mut self, attestation: &Attestation, - tls_public_key: Ed25519PublicKey, + tls_public_key: PublicKey, + account_public_key: PublicKey, tee_upgrade_deadline_duration: Duration, ) -> TeeQuoteStatus { - let expected_report_data = ReportData::V1(ReportDataV1::new(*tls_public_key.as_bytes())); + // Recreate the exact same ReportData that the enclave produced + let expected_report_data = + ReportData::V1(ReportDataV1::new(tls_public_key, account_public_key)); + let is_valid = attestation.verify( expected_report_data, Self::current_time_seconds(), @@ -130,12 +156,11 @@ impl TeeState { return TeeQuoteStatus::Invalid; }; - let tls_public_key = match node_id.tls_public_key.clone().try_into_dto_type() { - Ok(value) => value, - Err(_) => return TeeQuoteStatus::Invalid, - }; + let expected_report_data = ReportData::new( + node_id.tls_public_key.clone(), + node_id.account_public_key.clone(), + ); - let expected_report_data = ReportData::V1(ReportDataV1::new(*tls_public_key.as_bytes())); let time_stamp_seconds = Self::current_time_seconds(); let quote_result = participant_attestation.verify( @@ -167,13 +192,20 @@ impl TeeState { .filter(|(account_id, _, participant_info)| { let tls_public_key = participant_info.sign_pk.clone(); - let tee_status = self.verify_tee_participant( - &NodeId { - account_id: account_id.clone(), - tls_public_key, - }, - tee_upgrade_deadline_duration, - ); + // Try to find the existing NodeId for this participant (if attestation was recorded) + let maybe_node = self.find_node_id_by_tls_key(&tls_public_key); + + let node_id = NodeId { + account_id: account_id.clone(), + tls_public_key: tls_public_key.clone(), + // If we’re still in transition (mock attestation), allow None. + // TODO(#823): remove this fallback once all MPC nodes are required + // to run inside a TEE and provide a valid account_public_key. + account_public_key: maybe_node.and_then(|n| n.account_public_key.clone()), + }; + + let tee_status = + self.verify_tee_participant(&node_id, tee_upgrade_deadline_duration); matches!(tee_status, TeeQuoteStatus::Valid) }) @@ -255,6 +287,7 @@ impl TeeState { .map(|(account_id, _, p_info)| NodeId { account_id: account_id.clone(), tls_public_key: p_info.sign_pk.clone(), + account_public_key: None, //mapping to NodeId without account_public_key. }) .collect(); @@ -277,6 +310,85 @@ impl TeeState { pub fn get_tee_accounts(&self) -> Vec { self.participants_attestations.keys().cloned().collect() } + + /// Find a NodeId by its TLS public key. + pub fn find_node_id_by_tls_key(&self, tls_public_key: &PublicKey) -> Option { + self.participants_attestations + .keys() + .find(|node_id| &node_id.tls_public_key == tls_public_key) + .cloned() + } + /// Returns true if the caller belongs to an attested node. + /// + /// Transition phase (non-TEE β†’ TEE): + /// - If `account_public_key` is `Some`, we validate against the signer public key. + /// - If `account_public_key` is `None`, we fall back to account_id (legacy mock nodes). + /// + /// TODO (Migration plan): + /// 1. [Current] Allow `None` and fall back to account_id for legacy mock nodes. + /// 2. [Final phase] Remove the `None` branch entirely, making TEE attestation mandatory. + pub fn is_caller_an_attested_node(&self) -> bool { + let signer_pk = env::signer_account_pk(); + let signer_id = env::signer_account_id(); + let predecessor = env::predecessor_account_id(); + + println!("println {}", predecessor); + env::log_str(&format!("predecessor: {:?}", predecessor)); + + // Print to both NEAR logs and stdout for visibility in tests / VS Code + env::log_str(&format!( + "signer_id: {:?}, signer_pk: {:?}", + signer_id, signer_pk + )); + println!("signer_id: {:?}, signer_pk: {:?}", signer_id, signer_pk); + + env::log_str(&format!( + "Number of participant attestations: {}", + self.participants_attestations.len() + )); + + let keys: Vec = self + .participants_attestations + .keys() + .map(|node_id| { + format!( + "account_id: {}, tls_pk: {:?}, account_pk: {:?}", + node_id.account_id, node_id.tls_public_key, node_id.account_public_key + ) + }) + .collect(); + + env::log_str(&format!( + "participants_attestations keys:\n{}", + keys.join("\n") + )); + + let result = self.participants_attestations.keys().any(|node_id| { + match &node_id.account_public_key { + Some(pk) => pk == &signer_pk, + None => { + println!( + "legacy check (mock node): comparing account_id {:?} == {:?}", + node_id.account_id, signer_id + ); + node_id.account_id == signer_id // TODO (#823): legacy mock node (temporary) β€” remove after transition + } + } + }); + + println!("println! is_caller_an_attested_node result = {}", result); + env::log_str(&format!("is_caller_an_attested_node result = {}", result)); + + result + } + + /// Panics if the caller is not an attested MPC node + pub fn assert_caller_is_attested_node(&self) { + assert!( + self.is_caller_an_attested_node(), + "Caller is not an attested MPC node" + ); + } } #[cfg(test)] @@ -302,6 +414,7 @@ mod tests { .map(|(account_id, _, p_info)| NodeId { account_id: account_id.clone(), tls_public_key: p_info.sign_pk.clone(), + account_public_key: Some(bogus_ed25519_near_public_key()), //TODO check if this is ok. }) .collect(); @@ -310,6 +423,7 @@ mod tests { let non_participant_uid = NodeId { account_id: non_participant.clone(), + account_public_key: Some(bogus_ed25519_near_public_key()), tls_public_key: bogus_ed25519_near_public_key(), }; for node_id in &participant_nodes { diff --git a/crates/contract/tests/inprocess/expired_attestation.rs b/crates/contract/tests/inprocess/expired_attestation.rs index c646ffe39..dc62e4095 100644 --- a/crates/contract/tests/inprocess/expired_attestation.rs +++ b/crates/contract/tests/inprocess/expired_attestation.rs @@ -93,13 +93,16 @@ impl TestSetup { self.contract.vote_code_hash(hash.into()).unwrap(); } } - + // Returns the list of NodeIds for all participants + // Note that the account_public_key field in NodeId is None. + // This is because NodeId is used in contexts where account_public_key is not needed. fn get_participant_node_ids(&self) -> Vec { self.participants_list .iter() .map(|(account_id, _, participant_info)| NodeId { account_id: account_id.clone(), tls_public_key: participant_info.sign_pk.clone(), + account_public_key: None, }) .collect() } @@ -165,6 +168,7 @@ fn test_participant_kickout_after_expiration() { .map(|(account_id, _, participant_info)| NodeId { account_id, tls_public_key: participant_info.sign_pk, + account_public_key: Some(bogus_ed25519_near_public_key()), }) .collect(); @@ -182,6 +186,7 @@ fn test_participant_kickout_after_expiration() { let third_node = NodeId { account_id: setup.participants_list[2].0.clone(), tls_public_key: setup.participants_list[2].2.sign_pk.clone(), + account_public_key: Some(bogus_ed25519_near_public_key()), }; setup.submit_attestation_for_node(&third_node, expiring_attestation); @@ -264,6 +269,7 @@ fn test_clean_tee_status_removes_non_participants() { .map(|(account_id, _, participant_info)| NodeId { account_id, tls_public_key: participant_info.sign_pk, + account_public_key: Some(bogus_ed25519_near_public_key()), }) .collect(); @@ -276,6 +282,7 @@ fn test_clean_tee_status_removes_non_participants() { let removed_participant_node = NodeId { account_id: "removed.participant.near".parse().unwrap(), tls_public_key: bogus_ed25519_near_public_key(), + account_public_key: Some(bogus_ed25519_near_public_key()), }; setup.submit_attestation_for_node(&removed_participant_node, valid_attestation); diff --git a/crates/contract/tests/sandbox/ckd.rs b/crates/contract/tests/sandbox/ckd.rs index 7673bd890..30b4e608c 100644 --- a/crates/contract/tests/sandbox/ckd.rs +++ b/crates/contract/tests/sandbox/ckd.rs @@ -20,7 +20,9 @@ async fn create_account_given_id( #[tokio::test] async fn test_contract_ckd_request() -> anyhow::Result<()> { - let (worker, contract, _, sks) = init_env_secp256k1(1).await; + let (worker, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; + let sk = match &sks[0] { crate::sandbox::common::SharedSecretKey::Secp256k1(sk) => sk, crate::sandbox::common::SharedSecretKey::Ed25519(_) => unreachable!(), @@ -56,6 +58,7 @@ async fn test_contract_ckd_request() -> anyhow::Result<()> { &request, Some((&respond_req, &respond_resp)), &contract, + attested_account, ) .await?; } @@ -78,6 +81,7 @@ async fn test_contract_ckd_request() -> anyhow::Result<()> { &request, Some((&respond_req, &respond_resp)), &contract, + attested_account, ) .await?; derive_confidential_key_and_validate( @@ -85,13 +89,15 @@ async fn test_contract_ckd_request() -> anyhow::Result<()> { &request, Some((&respond_req, &respond_resp)), &contract, + attested_account, ) .await?; // Check that a ckd with no response from MPC network properly errors out: - let err = derive_confidential_key_and_validate(account, &request, None, &contract) - .await - .expect_err("should have failed with timeout"); + let err = + derive_confidential_key_and_validate(account, &request, None, &contract, attested_account) + .await + .expect_err("should have failed with timeout"); assert!(err .to_string() .contains(&errors::RequestError::Timeout.to_string())); @@ -101,7 +107,9 @@ async fn test_contract_ckd_request() -> anyhow::Result<()> { #[tokio::test] async fn test_contract_ckd_success_refund() -> anyhow::Result<()> { - let (worker, contract, _, sks) = init_env_secp256k1(1).await; + let (worker, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; + let alice = worker.dev_create_account().await?; let balance = alice.view_account().await?.balance; let contract_balance = contract.view_account().await?.balance; @@ -130,9 +138,9 @@ async fn test_contract_ckd_success_refund() -> anyhow::Result<()> { dbg!(&status); tokio::time::sleep(std::time::Duration::from_secs(3)).await; - // Call `respond_ckd` as if we are the MPC network itself. - let respond = contract - .call("respond_ckd") + // Call `respond_ckd` as an attested node: + let respond = attested_account + .call(contract.id(),"respond_ckd") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -234,7 +242,9 @@ async fn test_contract_ckd_fail_refund() -> anyhow::Result<()> { #[tokio::test] async fn test_contract_ckd_request_deposits() -> anyhow::Result<()> { - let (worker, contract, _, sks) = init_env_secp256k1(1).await; + let (worker, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; + let alice = worker.dev_create_account().await?; let sk = match &sks[0] { crate::sandbox::common::SharedSecretKey::Secp256k1(sk) => sk, @@ -260,8 +270,8 @@ async fn test_contract_ckd_request_deposits() -> anyhow::Result<()> { create_response_ckd(alice.id(), app_public_key, &request.domain_id, sk); // Responding to the request should fail with missing request because the deposit is too low, // so the request should have never made it into the request queue and subsequently the MPC network. - let respond = contract - .call("respond_ckd") + let respond = attested_account + .call(contract.id(),"respond_ckd") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp diff --git a/crates/contract/tests/sandbox/common.rs b/crates/contract/tests/sandbox/common.rs index 04f905bb0..25e73b3b5 100644 --- a/crates/contract/tests/sandbox/common.rs +++ b/crates/contract/tests/sandbox/common.rs @@ -83,7 +83,7 @@ pub const GAS_FOR_VOTE_RESHARED: Gas = Gas::from_tgas(15); /// not be getting that big. /// /// TODO(#771): Reduce this to the minimal value possible after #770 is resolved -pub const CURRENT_CONTRACT_DEPLOY_DEPOSIT: NearToken = NearToken::from_millinear(11263); +pub const CURRENT_CONTRACT_DEPLOY_DEPOSIT: NearToken = NearToken::from_millinear(11195); pub fn candidates(names: Option>) -> Participants { let mut participants: Participants = Participants::new(); @@ -184,33 +184,82 @@ fn load_contract(package_name: &str) -> Vec { } }; + use std::fs; + use std::path::Path; + use std::process::Command; + if do_build { + println!("πŸ”§ Starting build for package: {package_name}"); + println!("πŸ“‚ Project directory: {}", project_dir.display()); + + // --- Step 1: Build the WASM --- + let cargo_args = [ + "build", + &format!("--package={package_name}"), + "--profile=release-contract", + "--target=wasm32-unknown-unknown", + ]; + println!("πŸš€ Running: cargo {}", cargo_args.join(" ")); + let status = Command::new("cargo") - .args([ - "build", - &format!("--package={package_name}"), - "--profile=release-contract", - "--target=wasm32-unknown-unknown", - ]) + .args(&cargo_args) .current_dir(&project_dir) .status() - .expect("Failed to run cargo build"); + .expect("❌ Failed to run cargo build"); + + assert!(status.success(), "❌ cargo build failed"); + + println!("βœ… cargo build succeeded"); + + // --- Step 2: Locate and optimize the WASM --- + println!("πŸ”Ž Expected wasm output path: {}", wasm_path.display()); + if wasm_path.exists() { + let size = fs::metadata(&wasm_path).map(|m| m.len()).unwrap_or(0); + println!("βœ… Found wasm file ({} bytes)", size); + } else { + println!("⚠️ wasm file not found at expected path! Check cargo output directory."); + } + + // Check if wasm-opt exists in PATH + let check = Command::new("bash") + .arg("-c") + .arg("command -v wasm-opt") + .output() + .expect("Failed to check for wasm-opt in PATH"); + + if check.status.success() { + let path_str = String::from_utf8_lossy(&check.stdout).trim().to_string(); + println!("🧠 Using wasm-opt from: {}", path_str); + } else { + println!("❌ wasm-opt not found in PATH"); + } - assert!(status.success(), "cargo build failed"); + let wasm_opt_args = [ + "--enable-bulk-memory", + "-Oz", + "-o", + wasm_path.to_str().unwrap(), + wasm_path.to_str().unwrap(), + ]; + println!("πŸƒ Running: wasm-opt {}", wasm_opt_args.join(" ")); let status = Command::new("wasm-opt") - .args([ - "--enable-bulk-memory", - "-Oz", - "-o", - wasm_path.to_str().unwrap(), - wasm_path.to_str().unwrap(), - ]) + .args(&wasm_opt_args) .current_dir(&project_dir) .status() - .expect("Failed to run wasm-opt"); + .expect("❌ Failed to run wasm-opt (binary not found?)"); + + assert!(status.success(), "❌ wasm-opt failed"); + println!( + "βœ… wasm-opt optimization succeeded for {}", + wasm_path.display() + ); - assert!(status.success(), "wasm-opt failed"); + // --- Step 3: Update lockfile --- + lockfile.set_len(0).unwrap(); + lockfile + .write_all(serde_json::to_string(&BuildLock::new()).unwrap().as_bytes()) + .expect("Failed to write timestamp to lockfile"); lockfile.set_len(0).unwrap(); lockfile @@ -562,10 +611,11 @@ pub async fn submit_signature_response( respond_req: &SignatureRequest, respond_resp: &SignatureResponse, contract: &Contract, + attested_account: &Account, ) -> anyhow::Result<()> { // Call `respond` as if we are the MPC network itself. - let respond = contract - .call("respond") + let respond = attested_account + .call(contract.id(),"respond") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -582,13 +632,14 @@ pub async fn sign_and_validate( request: &SignRequestArgs, respond: Option<(&SignatureRequest, &SignatureResponse)>, contract: &Contract, + attested_account: &Account, ) -> anyhow::Result<()> { let status = submit_sign_request(request, contract).await?; tokio::time::sleep(std::time::Duration::from_secs(3)).await; if let Some((respond_req, respond_resp)) = respond { - submit_signature_response(respond_req, respond_resp, contract).await?; + submit_signature_response(respond_req, respond_resp, contract,attested_account).await?; } let execution = status.await?; @@ -653,12 +704,11 @@ pub async fn derive_confidential_key_and_validate( request: &CKDRequestArgs, respond: Option<(&CKDRequest, &CKDResponse)>, contract: &Contract, + attested_account: &Account, ) -> anyhow::Result<()> { let status = account .call(contract.id(), "request_app_private_key") - .args_json(serde_json::json!({ - "request": request, - })) + .args_json(serde_json::json!({ "request": request })) .deposit(NearToken::from_yoctonear(1)) .max_gas() .transact_async() @@ -668,9 +718,8 @@ pub async fn derive_confidential_key_and_validate( if let Some((respond_req, respond_resp)) = respond { assert!(account.id() == &respond_req.app_id); - // Call `respond_ckd` as if we are the MPC network itself. - let respond = contract - .call("respond_ckd") + let respond = attested_account + .call(contract.id(), "respond_ckd") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -684,8 +733,6 @@ pub async fn derive_confidential_key_and_validate( let execution = status.await?; dbg!(&execution); let execution = execution.into_result()?; - - // Finally wait the result: let returned_resp: CKDResponse = execution.json()?; if let Some((_, respond_resp)) = respond { assert_eq!( @@ -693,7 +740,6 @@ pub async fn derive_confidential_key_and_validate( "Returned ckd request does not match" ); } - Ok(()) } diff --git a/crates/contract/tests/sandbox/integration_tee_cleanup_after_resharing.rs b/crates/contract/tests/sandbox/integration_tee_cleanup_after_resharing.rs index 1b2227889..ba55dab67 100644 --- a/crates/contract/tests/sandbox/integration_tee_cleanup_after_resharing.rs +++ b/crates/contract/tests/sandbox/integration_tee_cleanup_after_resharing.rs @@ -58,6 +58,7 @@ async fn test_tee_cleanup_after_full_resharing_flow() -> Result<()> { let new_uid = NodeId { account_id: env_accounts[0].id().clone(), tls_public_key: bogus_ed25519_near_public_key(), + account_public_key: Some(bogus_ed25519_near_public_key()), }; let attestation = Attestation::Mock(MockAttestation::Valid); // todo #1109, add TLS key. submit_participant_info( diff --git a/crates/contract/tests/sandbox/sign.rs b/crates/contract/tests/sandbox/sign.rs index f95999c9e..2d04860b4 100644 --- a/crates/contract/tests/sandbox/sign.rs +++ b/crates/contract/tests/sandbox/sign.rs @@ -22,7 +22,9 @@ const DOMAIN_ID_ZERO: DomainId = DomainId(0); #[tokio::test] async fn test_contract_sign_request() -> anyhow::Result<()> { - let (_, contract, _, sks) = init_env_secp256k1(1).await; + let (_, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; + let predecessor_id = contract.id(); let path = "test"; @@ -46,7 +48,13 @@ async fn test_contract_sign_request() -> anyhow::Result<()> { ..Default::default() }; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; } // check duplicate requests can also be signed: @@ -65,11 +73,23 @@ async fn test_contract_sign_request() -> anyhow::Result<()> { domain_id: Some(DOMAIN_ID_ZERO), ..Default::default() }; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; // Check that a sign with no response from MPC network properly errors out: - let err = sign_and_validate(&request, None, &contract) + let err = sign_and_validate(&request, None, &contract, attested_account) .await .expect_err("should have failed with timeout"); assert!(err @@ -81,7 +101,8 @@ async fn test_contract_sign_request() -> anyhow::Result<()> { #[tokio::test] async fn test_contract_sign_success_refund() -> anyhow::Result<()> { - let (worker, contract, _, sks) = init_env_secp256k1(1).await; + let (worker, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; let alice = worker.dev_create_account().await?; let balance = alice.view_account().await?.balance; let contract_balance = contract.view_account().await?.balance; @@ -110,9 +131,9 @@ async fn test_contract_sign_success_refund() -> anyhow::Result<()> { dbg!(&status); tokio::time::sleep(std::time::Duration::from_secs(3)).await; - // Call `respond` as if we are the MPC network itself. - let respond = contract - .call("respond") + // Call `respond` as an attested node. + let respond = attested_account + .call(contract.id(),"respond") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -221,7 +242,8 @@ async fn test_contract_sign_fail_refund() -> anyhow::Result<()> { #[tokio::test] async fn test_contract_sign_request_deposits() -> anyhow::Result<()> { - let (_, contract, _, sks) = init_env_secp256k1(1).await; + let (_, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; let predecessor_id = contract.id(); let path = "testing-no-deposit"; @@ -249,8 +271,8 @@ async fn test_contract_sign_request_deposits() -> anyhow::Result<()> { // Responding to the request should fail with missing request because the deposit is too low, // so the request should have never made it into the request queue and subsequently the MPC network. - let respond = contract - .call("respond") + let respond = attested_account + .call(contract.id(), "respond") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -278,7 +300,8 @@ async fn test_contract_sign_request_deposits() -> anyhow::Result<()> { #[tokio::test] async fn test_sign_v1_compatibility() -> anyhow::Result<()> { - let (_, contract, _, sks) = init_env_secp256k1(1).await; + let (_, contract, mpc_nodes, sks) = init_env_secp256k1(1).await; + let attested_account = &mpc_nodes[0]; let predecessor_id = contract.id(); let path = "test"; @@ -313,8 +336,8 @@ async fn test_sign_v1_compatibility() -> anyhow::Result<()> { tokio::time::sleep(std::time::Duration::from_secs(3)).await; // Call `respond` as if we are the MPC network itself. - let respond = contract - .call("respond") + let respond = attested_account + .call(contract.id(), "respond") .args_json(serde_json::json!({ "request": respond_req, "response": respond_resp @@ -393,7 +416,8 @@ async fn test_contract_initialization() -> anyhow::Result<()> { #[tokio::test] async fn test_contract_sign_request_eddsa() -> anyhow::Result<()> { - let (_, contract, _, sks) = init_env_ed25519(1).await; + let (_, contract, mpc_nodes, sks) = init_env_ed25519(1).await; + let attested_account = &mpc_nodes[0]; let predecessor_id = contract.id(); let path = "test"; @@ -418,7 +442,13 @@ async fn test_contract_sign_request_eddsa() -> anyhow::Result<()> { ..Default::default() }; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; } // check duplicate requests can also be signed: @@ -437,11 +467,23 @@ async fn test_contract_sign_request_eddsa() -> anyhow::Result<()> { domain_id: Some(DomainId(0)), ..Default::default() }; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; - sign_and_validate(&request, Some((&respond_req, &respond_resp)), &contract).await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; + sign_and_validate( + &request, + Some((&respond_req, &respond_resp)), + &contract, + attested_account, + ) + .await?; // Check that a sign with no response from MPC network properly errors out: - let err = sign_and_validate(&request, None, &contract) + let err = sign_and_validate(&request, None, &contract, attested_account) .await .expect_err("should have failed with timeout"); assert!(err diff --git a/crates/contract/tests/sandbox/upgrade_to_current_contract.rs b/crates/contract/tests/sandbox/upgrade_to_current_contract.rs index d06f81b7a..d97b0c6d0 100644 --- a/crates/contract/tests/sandbox/upgrade_to_current_contract.rs +++ b/crates/contract/tests/sandbox/upgrade_to_current_contract.rs @@ -180,6 +180,8 @@ async fn upgrade_preserves_state_and_requests( let contract = deploy_old(&worker, network).await.unwrap(); let (accounts, participants) = init_old_contract(worker, &contract).await.unwrap(); + let attested_account = &accounts[0]; + let injected_contract_state = execute_key_generation_and_add_random_state(&accounts, participants, &contract).await; @@ -199,12 +201,13 @@ async fn upgrade_preserves_state_and_requests( state_pre_upgrade, state_post_upgrade, "State of the contract should remain the same post upgrade." ); - + //TODO: need to add attested_account to the function parameters for pending in injected_contract_state.pending_sign_requests { submit_signature_response( &pending.signature_request, &pending.signature_response, &contract, + attested_account, ) .await .unwrap(); diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 16bc3711e..db93bc61b 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -251,7 +251,12 @@ impl StartCmd { // Generate attestation let tee_authority = TeeAuthority::try_from(self.tee_authority.clone())?; let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); - let report_data = ReportData::new(*tls_public_key.into_dto_type().as_bytes()); + let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); + + let report_data = ReportData::new( + tls_public_key.clone().to_near_sdk_public_key()?, + Some(account_public_key.clone().to_near_sdk_public_key()?), + ); let attestation = tee_authority.generate_attestation(report_data).await?; // Create communication channels and runtime @@ -372,6 +377,7 @@ impl StartCmd { .map_err(|_| anyhow!("Root task handle was already set"))?; let tls_public_key = secrets.persistent_secrets.p2p_private_key.verifying_key(); + let account_public_key = secrets.persistent_secrets.near_signer_key.verifying_key(); let secret_db = SecretDB::new(&home_dir.join("assets"), secrets.local_storage_aes_key)?; @@ -401,10 +407,10 @@ impl StartCmd { let tee_authority_clone = tee_authority.clone(); tokio::spawn(async move { if let Err(e) = periodic_attestation_submission( - tee_authority_clone, + tee_authority, tx_sender_clone, tls_public_key, - tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), + account_public_key, ) .await { diff --git a/crates/node/src/tee/remote_attestation.rs b/crates/node/src/tee/remote_attestation.rs index 9dba26198..df0fe9956 100644 --- a/crates/node/src/tee/remote_attestation.rs +++ b/crates/node/src/tee/remote_attestation.rs @@ -89,27 +89,16 @@ pub async fn periodic_attestation_submission anyhow::Result<()> { - let tls_sdk_public_key = *tls_public_key.into_dto_type().as_bytes(); - let report_data = ReportData::new(tls_sdk_public_key); - let fresh_attestation = tee_authority.generate_attestation(report_data).await?; - - loop { - interval_ticker.tick().await; - - submit_remote_attestation(tx_sender.clone(), fresh_attestation.clone(), tls_public_key) - .await?; - } -} - -/// Checks if TEE attestation is available for the given node in the TEE accounts list. -fn is_node_in_contract_tee_accounts( - tee_accounts_receiver: &mut watch::Receiver>, - node_id: &NodeId, -) -> bool { - let tee_accounts = tee_accounts_receiver.borrow_and_update(); - tee_accounts.contains(node_id) + periodic_attestation_submission_with_interval( + tee_authority, + tx_sender, + tls_public_key, + account_public_key, + tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), + ) + .await } /// Monitors the contract for TEE attestation removal and triggers resubmission when needed. @@ -121,7 +110,8 @@ pub async fn monitor_attestation_removal( tee_authority: TeeAuthority, tx_sender: T, tls_public_key: VerifyingKey, - mut tee_accounts_receiver: watch::Receiver>, + account_public_key: VerifyingKey, + mut interval_ticker: I, ) -> anyhow::Result<()> { let node_id = NodeId { account_id: node_account_id.clone(), @@ -132,8 +122,23 @@ pub async fn monitor_attestation_removal( .map_err(|e| anyhow::anyhow!("Failed to create PublicKey from TLS public key: {}", e))?, }; - let initially_available = - is_node_in_contract_tee_accounts(&mut tee_accounts_receiver, &node_id); + let tls_sdk_public_key = tls_public_key.to_near_sdk_public_key()?; + let account_sdk_public_key = account_public_key.to_near_sdk_public_key()?; + + let report_data = ReportData::new( + tls_sdk_public_key.clone(), + Some(account_sdk_public_key.clone()), + ); + let fresh_attestation = match tee_authority.generate_attestation(report_data).await { + Ok(attestation) => attestation, + Err(error) => { + tracing::error!( + ?error, + "failed to generate fresh attestation, skipping this cycle" + ); + continue; + } + }; tracing::info!( %node_account_id, @@ -292,10 +297,13 @@ mod tests { let mut rng = rand::rngs::StdRng::seed_from_u64(42); let key = SigningKey::generate(&mut rng).verifying_key(); - let handle = tokio::spawn(periodic_attestation_submission( + let account_key = SigningKey::generate(&mut rng).verifying_key(); + + let handle = tokio::spawn(periodic_attestation_submission_with_interval( tee_authority, sender.clone(), key, + account_key, MockTicker::new(TEST_SUBMISSION_COUNT), )); diff --git a/crates/tee_authority/src/tee_authority.rs b/crates/tee_authority/src/tee_authority.rs index 8da42cc9d..3fac1efd9 100644 --- a/crates/tee_authority/src/tee_authority.rs +++ b/crates/tee_authority/src/tee_authority.rs @@ -282,8 +282,14 @@ mod tests { async fn test_generate_and_verify_attestation_local( #[values(true, false)] quote_verification_result: bool, ) { - let tls_key = p2p_tls_key(); - let report_data = ReportData::V1(ReportDataV1::new(tls_key)); + let tls_key = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847" + .parse() + .unwrap(); + + let account_key = "ed25519:5v8Y8ZLoxZzCVtYpjh1cYdFrRh1p9EXAMPLEaQJ5sP4o" + .parse() + .unwrap(); + let report_data = ReportData::V1(ReportDataV1::new(tls_key, account_key)); let authority = TeeAuthority::Local(LocalTeeAuthorityConfig::new(quote_verification_result)); diff --git a/crates/test_utils/assets/near_account_public_key.pub b/crates/test_utils/assets/near_account_public_key.pub new file mode 100644 index 000000000..b3a85d5f7 --- /dev/null +++ b/crates/test_utils/assets/near_account_public_key.pub @@ -0,0 +1 @@ +ed25519:4oQf1aYqPvfTPL71bNXZZ6hMYeKGaMW8JEKXanP7AAAA \ No newline at end of file diff --git a/crates/test_utils/src/attestation.rs b/crates/test_utils/src/attestation.rs index 6155dcb4e..c9e4a602e 100644 --- a/crates/test_utils/src/attestation.rs +++ b/crates/test_utils/src/attestation.rs @@ -57,6 +57,11 @@ pub fn near_p2p_tls_key() -> near_sdk::PublicKey { key_file.parse().expect("File contains a valid public key") } +pub fn account_key() -> PublicKey { + let key_file = include_str!("../assets/near_account_public_key.pub"); + key_file.parse().expect("File contains a valid public key") +} + pub fn mock_dstack_attestation() -> Attestation { let quote = quote(); let collateral_json_string = include_str!("../assets/collateral.json");