diff --git a/Cargo.lock b/Cargo.lock index 3b93c9be02..2c3058a06d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5848,6 +5848,7 @@ version = "3.13.0" dependencies = [ "anyhow", "assert_matches", + "attestation", "blstrs", "borsh", "cargo-near-build", @@ -5882,6 +5883,7 @@ dependencies = [ "serde_with", "sha2 0.10.9", "signature", + "tee-verifier-interface", "test-utils", "thiserror 2.0.18", "threshold-signatures", diff --git a/crates/attestation/src/attestation.rs b/crates/attestation/src/attestation.rs index 457cc14ab8..a595e183cd 100644 --- a/crates/attestation/src/attestation.rs +++ b/crates/attestation/src/attestation.rs @@ -35,6 +35,7 @@ pub(crate) const KEY_PROVIDER_EVENT: &str = "key-provider"; const RTMR3_INDEX: u32 = 3; #[derive(Clone, Constructor, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] pub struct DstackAttestation { pub quote: QuoteBytes, pub collateral: Collateral, diff --git a/crates/attestation/src/tcb_info.rs b/crates/attestation/src/tcb_info.rs index 06acde3dd3..4d8fb4c2fc 100644 --- a/crates/attestation/src/tcb_info.rs +++ b/crates/attestation/src/tcb_info.rs @@ -1,4 +1,6 @@ use alloc::string::String; +#[cfg(feature = "borsh-schema")] +use alloc::string::ToString; use alloc::vec::Vec; use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(any(test, feature = "dstack-conversions"))] @@ -16,6 +18,7 @@ pub enum ParsingError { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] pub struct TcbInfo { pub mrtd: HexBytes<48>, pub rtmr0: HexBytes<48>, @@ -32,6 +35,7 @@ pub struct TcbInfo { #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] pub struct EventLog { pub imr: u32, pub event_type: u32, @@ -60,6 +64,33 @@ pub struct EventLog { #[serde(transparent)] pub struct HexBytes(#[serde_as(as = "Hex")] [u8; N]); +/// Manual impl because the derive drops the const parameter from the +/// declaration, so `HexBytes<48>` and `HexBytes<32>` in one schema collide +/// ("Redefining type schema for HexBytes"). +#[cfg(feature = "borsh-schema")] +impl borsh::BorshSchema for HexBytes { + fn declaration() -> borsh::schema::Declaration { + alloc::format!("HexBytes<{N}>") + } + + fn add_definitions_recursively( + definitions: &mut alloc::collections::BTreeMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, + ) { + let fields = borsh::schema::Fields::UnnamedFields(alloc::vec![ + <[u8; N] as borsh::BorshSchema>::declaration() + ]); + borsh::schema::add_definition( + Self::declaration(), + borsh::schema::Definition::Struct { fields }, + definitions, + ); + <[u8; N] as borsh::BorshSchema>::add_definitions_recursively(definitions); + } +} + #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum HexBytesOrEmpty { @@ -198,6 +229,17 @@ mod tests { use rstest::rstest; use serde_json; + /// `TcbInfo` holds both `HexBytes<48>` and `HexBytes<32>`; schema + /// generation panics if their declarations collide. + #[cfg(feature = "borsh-schema")] + #[test] + fn TcbInfo__should_generate_borsh_schema() { + let container = borsh::schema_container_of::(); + + assert!(container.get_definition("HexBytes<48>").is_some()); + assert!(container.get_definition("HexBytes<32>").is_some()); + } + #[test] fn TcbInfo__should_deserialize_from_real_test_data() { // Given diff --git a/crates/chain-gateway/src/transaction_sender/signer.rs b/crates/chain-gateway/src/transaction_sender/signer.rs index c955f5d18f..a7741d4f6a 100644 --- a/crates/chain-gateway/src/transaction_sender/signer.rs +++ b/crates/chain-gateway/src/transaction_sender/signer.rs @@ -144,7 +144,7 @@ mod tests { method_name: "do_something".to_string(), args: b"test args".to_vec(), gas: TEST_GAS, - deposit: NearToken::from_yoctonear(0), + deposit: NearToken::from_near(0), } } diff --git a/crates/chain-gateway/src/transaction_sender/traits.rs b/crates/chain-gateway/src/transaction_sender/traits.rs index 99d52c1744..4b173903e7 100644 --- a/crates/chain-gateway/src/transaction_sender/traits.rs +++ b/crates/chain-gateway/src/transaction_sender/traits.rs @@ -86,7 +86,7 @@ mod tests { let mut args = vec![0u8; 16]; rng.fill(&mut args[..]); let gas = NearGas::from_gas(300); - let deposit = NearToken::from_yoctonear(0); + let deposit = NearToken::from_near(0); ( receiver_id, FunctionCallArgs { diff --git a/crates/contract/Cargo.toml b/crates/contract/Cargo.toml index f4764c7d04..8c2929c197 100644 --- a/crates/contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -65,6 +65,7 @@ abi = [ "near-mpc-contract-interface/abi", "mpc-attestation/abi", "mpc-primitives/abi", + "tee-verifier-interface/borsh-schema", "schemars", ] # This is used when running `cargo clippy --all-features`, because otherwise `abi` feat will break compilation. @@ -74,6 +75,7 @@ __abi-generate = ["abi", "near-sdk/__abi-generate"] [dependencies] assert_matches = { workspace = true } +attestation = { workspace = true } blstrs = { workspace = true } borsh = { workspace = true } curve25519-dalek = { workspace = true } @@ -87,7 +89,7 @@ k256 = { workspace = true, features = [ "arithmetic", "expose-field", ] } -mpc-attestation = { workspace = true, features = ["local-verify"] } +mpc-attestation = { workspace = true } mpc-primitives = { workspace = true } near-account-id = { workspace = true, features = ["serde"] } near-mpc-bounded-collections = { workspace = true } @@ -102,6 +104,7 @@ rand = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } +tee-verifier-interface = { workspace = true } thiserror = { workspace = true } threshold-signatures = { workspace = true, optional = true } diff --git a/crates/contract/src/config.rs b/crates/contract/src/config.rs index 9acf2a28ca..ef952f116e 100644 --- a/crates/contract/src/config.rs +++ b/crates/contract/src/config.rs @@ -21,6 +21,8 @@ const DEFAULT_RETURN_SIGNATURE_AND_CLEAN_STATE_ON_SUCCESS_CALL_TERA_GAS: u64 = 7 const DEFAULT_RETURN_CK_AND_CLEAN_STATE_ON_SUCCESS_CALL_TERA_GAS: u64 = 7; /// Prepaid gas for a `fail_on_timeout` call const DEFAULT_FAIL_ON_TIMEOUT_TERA_GAS: u64 = 2; +/// Prepaid gas for a `fail_attestation_submission` call +const DEFAULT_FAIL_ATTESTATION_SUBMISSION_TERA_GAS: u64 = 2; /// Prepaid gas for a `clean_tee_status` call const DEFAULT_CLEAN_TEE_STATUS_TERA_GAS: u64 = 10; /// Prepaid gas for the reshare-time `clean_invalid_attestations` promise. @@ -34,6 +36,11 @@ const DEFAULT_REMOVE_NON_PARTICIPANT_UPDATE_VOTES_TERA_GAS: u64 = 5; const DEFAULT_CLEAN_FOREIGN_CHAIN_DATA_TERA_GAS: u64 = 5; /// Prepaid gas for a `remove_non_participant_tee_verifier_votes` call const DEFAULT_REMOVE_NON_PARTICIPANT_TEE_VERIFIER_VOTES_TERA_GAS: u64 = 5; +/// Gas attached to the cross-contract `verify_quote` call on the TEE verifier. +const DEFAULT_VERIFIER_TERA_GAS: u64 = 100; +/// Prepaid gas for the `resolve_verification` callback. Carries the bulk of the +/// post-DCAP work (allowlist match, RTMR3 replay, app-compose validation, store). +const DEFAULT_RESOLVE_VERIFICATION_TERA_GAS: u64 = 60; /// Config for V2 of the contract. #[near(serializers=[borsh, json])] @@ -56,6 +63,8 @@ pub(crate) struct Config { pub(crate) return_ck_and_clean_state_on_success_call_tera_gas: u64, /// Prepaid gas for a `fail_on_timeout` call. pub(crate) fail_on_timeout_tera_gas: u64, + /// Prepaid gas for a `fail_attestation_submission` call. + pub(crate) fail_attestation_submission_tera_gas: u64, /// Prepaid gas for a `clean_tee_status` call. pub(crate) clean_tee_status_tera_gas: u64, /// Prepaid gas for the reshare-time `clean_invalid_attestations` promise. @@ -68,6 +77,10 @@ pub(crate) struct Config { pub(crate) clean_foreign_chain_data_tera_gas: u64, /// Prepaid gas for a `remove_non_participant_tee_verifier_votes` call. pub(crate) remove_non_participant_tee_verifier_votes_tera_gas: u64, + /// Gas attached to the cross-contract `verify_quote` call on the verifier. + pub(crate) verifier_tera_gas: u64, + /// Prepaid gas for the `resolve_verification` callback. + pub(crate) resolve_verification_tera_gas: u64, } impl Default for Config { @@ -85,6 +98,7 @@ impl Default for Config { return_ck_and_clean_state_on_success_call_tera_gas: DEFAULT_RETURN_CK_AND_CLEAN_STATE_ON_SUCCESS_CALL_TERA_GAS, fail_on_timeout_tera_gas: DEFAULT_FAIL_ON_TIMEOUT_TERA_GAS, + fail_attestation_submission_tera_gas: DEFAULT_FAIL_ATTESTATION_SUBMISSION_TERA_GAS, clean_tee_status_tera_gas: DEFAULT_CLEAN_TEE_STATUS_TERA_GAS, clean_invalid_attestations_tera_gas: DEFAULT_CLEAN_INVALID_ATTESTATIONS_TERA_GAS, cleanup_orphaned_node_migrations_tera_gas: @@ -94,6 +108,8 @@ impl Default for Config { clean_foreign_chain_data_tera_gas: DEFAULT_CLEAN_FOREIGN_CHAIN_DATA_TERA_GAS, remove_non_participant_tee_verifier_votes_tera_gas: DEFAULT_REMOVE_NON_PARTICIPANT_TEE_VERIFIER_VOTES_TERA_GAS, + verifier_tera_gas: DEFAULT_VERIFIER_TERA_GAS, + resolve_verification_tera_gas: DEFAULT_RESOLVE_VERIFICATION_TERA_GAS, } } } diff --git a/crates/contract/src/dto_mapping.rs b/crates/contract/src/dto_mapping.rs index 104870e261..38d277ce00 100644 --- a/crates/contract/src/dto_mapping.rs +++ b/crates/contract/src/dto_mapping.rs @@ -472,6 +472,9 @@ impl From for Config { if let Some(v) = config_ext.fail_on_timeout_tera_gas { config.fail_on_timeout_tera_gas = v; } + if let Some(v) = config_ext.fail_attestation_submission_tera_gas { + config.fail_attestation_submission_tera_gas = v; + } if let Some(v) = config_ext.clean_tee_status_tera_gas { config.clean_tee_status_tera_gas = v; } @@ -490,6 +493,12 @@ impl From for Config { if let Some(v) = config_ext.remove_non_participant_tee_verifier_votes_tera_gas { config.remove_non_participant_tee_verifier_votes_tera_gas = v; } + if let Some(v) = config_ext.verifier_tera_gas { + config.verifier_tera_gas = v; + } + if let Some(v) = config_ext.resolve_verification_tera_gas { + config.resolve_verification_tera_gas = v; + } config } @@ -510,6 +519,7 @@ impl From<&Config> for near_mpc_contract_interface::types::Config { return_ck_and_clean_state_on_success_call_tera_gas: value .return_ck_and_clean_state_on_success_call_tera_gas, fail_on_timeout_tera_gas: value.fail_on_timeout_tera_gas, + fail_attestation_submission_tera_gas: value.fail_attestation_submission_tera_gas, clean_tee_status_tera_gas: value.clean_tee_status_tera_gas, clean_invalid_attestations_tera_gas: value.clean_invalid_attestations_tera_gas, cleanup_orphaned_node_migrations_tera_gas: value @@ -519,6 +529,8 @@ impl From<&Config> for near_mpc_contract_interface::types::Config { clean_foreign_chain_data_tera_gas: value.clean_foreign_chain_data_tera_gas, remove_non_participant_tee_verifier_votes_tera_gas: value .remove_non_participant_tee_verifier_votes_tera_gas, + verifier_tera_gas: value.verifier_tera_gas, + resolve_verification_tera_gas: value.resolve_verification_tera_gas, } } } @@ -538,6 +550,7 @@ impl From for Config { return_ck_and_clean_state_on_success_call_tera_gas: value .return_ck_and_clean_state_on_success_call_tera_gas, fail_on_timeout_tera_gas: value.fail_on_timeout_tera_gas, + fail_attestation_submission_tera_gas: value.fail_attestation_submission_tera_gas, clean_tee_status_tera_gas: value.clean_tee_status_tera_gas, clean_invalid_attestations_tera_gas: value.clean_invalid_attestations_tera_gas, cleanup_orphaned_node_migrations_tera_gas: value @@ -547,6 +560,8 @@ impl From for Config { clean_foreign_chain_data_tera_gas: value.clean_foreign_chain_data_tera_gas, remove_non_participant_tee_verifier_votes_tera_gas: value .remove_non_participant_tee_verifier_votes_tera_gas, + verifier_tera_gas: value.verifier_tera_gas, + resolve_verification_tera_gas: value.resolve_verification_tera_gas, } } } diff --git a/crates/contract/src/errors.rs b/crates/contract/src/errors.rs index 7c67f0c100..a6fdc25201 100644 --- a/crates/contract/src/errors.rs +++ b/crates/contract/src/errors.rs @@ -1,6 +1,7 @@ use crate::crypto_shared::kdf::TweakNotOnCurve; use crate::primitives::domain::MIN_RECONSTRUCTION_THRESHOLD; use crate::primitives::key_state::{EpochId, Keyset}; +use crate::tee::tee_state::AttestationSubmissionError; use near_account_id::AccountId; use near_mpc_contract_interface::types as dtos; use near_mpc_contract_interface::types::{DomainId, DomainPurpose, ForeignChain, Protocol}; @@ -28,6 +29,14 @@ pub enum TeeError { "Due to previously failed TEE validation, the network is not accepting new requests at this point in time. Try again later." )] TeeValidationFailed, + #[error( + "No TEE verifier is configured yet. Participants must vote one in via vote_tee_verifier_change before Dstack attestations can be submitted." + )] + VerifierNotConfigured, + #[error("The TEE verifier rejected the quote: {reason}")] + QuoteRejected { reason: String }, + #[error("The TEE verifier did not answer the verify_quote call.")] + VerifierUnavailable, } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] @@ -318,6 +327,9 @@ pub enum Error { // Tee errors #[error(transparent)] NodeMigrationError(#[from] NodeMigrationError), + // Tee attestation submission errors + #[error(transparent)] + AttestationSubmission(#[from] AttestationSubmissionError), } impl near_sdk::FunctionError for Error { diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 68a4cd6aa7..64c6607831 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -49,6 +49,7 @@ use crate::{ }, storage_keys::StorageKey, tee::tee_state::{TeeQuoteStatus, TeeState}, + tee::verification_context::VerificationContext, tee::verifier_votes::{TeeVerifierVotes, VerifierChangeProposal}, update::{ProposedUpdates, Update, UpdateId}, }; @@ -71,6 +72,7 @@ use near_mpc_contract_interface::types::{ use near_mpc_contract_interface::{method_names, types::CKDRequestArgs}; use dtos::{Curve, DomainConfig, DomainId, DomainPurpose, Protocol}; +use mpc_attestation::attestation::{Attestation, DstackAttestation}; use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash, TeeVerifierCodeHash}; use near_sdk::{ AccountId, CryptoHash, Gas, GasWeight, NearToken, Promise, PromiseError, PromiseOrValue, env, @@ -87,11 +89,12 @@ use primitives::{ }; use tee::measurements::{ContractExpectedMeasurements, MeasurementVoteAction, MeasurementVotes}; use tee::proposal::{CodeHashesVotes, LauncherHashVotes}; +use tee_verifier_interface::{VerificationResult, VerifiedReport}; use state::{ProtocolContractState, running::RunningContractState}; use tee::{ proposal::{LauncherVoteAction, NodeImageHash}, - tee_state::{AttestationSubmissionError, NodeId, ParticipantInsertion, TeeValidationResult}, + tee_state::{NodeId, TeeValidationResult}, }; /// Register used to receive data id from `promise_await_data`. @@ -104,6 +107,12 @@ const MINIMUM_SIGN_REQUEST_DEPOSIT: NearToken = NearToken::from_yoctonear(1); /// Minimum deposit required for CKD requests const MINIMUM_CKD_REQUEST_DEPOSIT: NearToken = NearToken::from_yoctonear(1); +/// Flat fee a node attaches to [`MpcContract::submit_participant_info`] for its +/// stored attestation entry. The entry is bounded, so the fee is fixed and +/// nothing is refunded; its margin over the true cost absorbs storage-price and +/// layout changes. A unit test asserts it covers the worst-case entry. +const MINIMUM_ATTESTATION_STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(100); + /// Entries to scan in the post-reshare `clean_invalid_attestations` sweep. External /// callers may pick a different value; this only governs the automatic invocation. const RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN: u32 = 100; @@ -132,12 +141,15 @@ fn require_deposit(minimum_deposit: NearToken, predecessor: &AccountId) { .to_string(), ); } - Some(diff) => { - if diff > NearToken::from_yoctonear(0) { - log!("refund excess deposit {diff} to {predecessor}"); - Promise::new(predecessor.clone()).transfer(diff).detach(); - } - } + Some(diff) => refund_to(predecessor, diff), + } +} + +/// Transfers `amount` to `account_id` via a detached promise; no-op when zero. +fn refund_to(account_id: &AccountId, amount: NearToken) { + if amount > NearToken::from_near(0) { + log!("refund {amount} to {account_id}"); + Promise::new(account_id.clone()).transfer(amount).detach(); } } @@ -166,7 +178,9 @@ pub struct MpcContract { metrics: Metrics, foreign_chains: Lazy, /// The verifier contract account trusted for DCAP verification, or [`None`] - /// until participants vote one in. Not yet used to dispatch verification. + /// until participants vote one in. An [`Attestation::Dstack`] submission + /// offloads quote verification to this account; while it is [`None`], such + /// submissions are rejected with [`TeeError::VerifierNotConfigured`]. // TODO(#3639): once participants have voted a verifier in, make this // non-optional via a migration that requires it be set. tee_verifier_account_id: Option, @@ -754,15 +768,22 @@ impl MpcContract { ) } - /// (Prospective) Participants can submit their tee participant information through this - /// endpoint. + /// Submit a TEE attestation for a current or prospective participant. + /// + /// - [`Attestation::Mock`] is verified synchronously. + /// - [`Attestation::Dstack`] is verified asynchronously via a cross-contract + /// `verify_quote` call, with [`Self::resolve_verification`] chained as its + /// callback to run the post-DCAP checks and store the attestation. + /// + /// The caller must attach a flat 0.1 NEAR fee for the stored entry; the whole + /// fee is kept on success and refunded if the attestation is not accepted. #[payable] #[handle_result] pub fn submit_participant_info( &mut self, proposed_participant_attestation: dtos::Attestation, tls_public_key: dtos::Ed25519PublicKey, - ) -> Result<(), Error> { + ) -> Result, Error> { let proposed_participant_attestation = proposed_participant_attestation.try_into_contract_type()?; @@ -776,13 +797,6 @@ impl MpcContract { account_key ); - // Save the initial storage usage to know how much to charge the proposer for the storage - // used - let initial_storage = env::storage_usage(); - - let tee_upgrade_deadline_duration = - Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds); - // The node always signs submissions with an Ed25519 key // (`near_signer_key`), so the signer key here is Ed25519 in practice. // Reject non-Ed25519 signer keys rather than silently storing a value @@ -793,63 +807,67 @@ impl MpcContract { } })?; - // Add the participant information to the contract state - let attestation_insertion_result = self - .tee_state - .add_participant( - NodeId { - account_id: account_id.clone(), - tls_public_key, - account_public_key, - }, - proposed_participant_attestation, - tee_upgrade_deadline_duration, - ) - .map_err(|err| { - let reason = match &err { - AttestationSubmissionError::InvalidAttestation(_) => { - format!("TeeQuoteStatus is invalid: {err}") - } - AttestationSubmissionError::TlsKeyOwnedByOtherAccount => err.to_string(), - }; - InvalidParameters::InvalidTeeRemoteAttestation { reason } - })?; - - let caller_is_not_participant = self.voter_account().is_err(); - let is_new_attestation = matches!( - attestation_insertion_result, - ParticipantInsertion::NewlyInsertedParticipant - ); - - let attestation_storage_must_be_paid_by_caller = - is_new_attestation || caller_is_not_participant; - - if attestation_storage_must_be_paid_by_caller { - // `saturating_sub`: if a re-submission shrinks the entry, charge nothing - // rather than underflow. Intentional asymmetry: we do not refund freed bytes - // either — the caller already paid for the larger entry, and we'd rather - // accept that asymmetry than open a refund path for payload-shrinking games. - let storage_used = env::storage_usage().saturating_sub(initial_storage); - let cost = env::storage_byte_cost().saturating_mul(storage_used as u128); - let attached = env::attached_deposit(); + let node_id = NodeId { + account_id: account_id.clone(), + tls_public_key, + account_public_key, + }; - if attached < cost { - return Err(InvalidParameters::InsufficientDeposit { - attached: attached.as_yoctonear(), - required: cost.as_yoctonear(), - } - .into()); + let attached = env::attached_deposit(); + if attached < MINIMUM_ATTESTATION_STORAGE_DEPOSIT { + return Err(InvalidParameters::InsufficientDeposit { + attached: attached.as_yoctonear(), + required: MINIMUM_ATTESTATION_STORAGE_DEPOSIT.as_yoctonear(), } + .into()); + } - // Refund the difference if the proposer attached more than required - if let Some(diff) = attached.checked_sub(cost) - && diff > NearToken::from_yoctonear(0) - { - Promise::new(account_id).transfer(diff).detach(); + match proposed_participant_attestation { + Attestation::Mock(mock) => { + let tee_upgrade_deadline_duration = + Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds); + self.tee_state.verify_and_store_mock( + node_id, + mock, + tee_upgrade_deadline_duration, + )?; + Ok(PromiseOrValue::Value(())) } + Attestation::Dstack(attestation) => Ok(PromiseOrValue::Promise( + self.submit_dstack_attestation(node_id, attestation)?, + )), } + } - Ok(()) + /// Async [`Attestation::Dstack`] submission: spawns a promise calling + /// `verify_quote` on the trusted verifier contract, with + /// [`Self::resolve_verification`] chained as its callback. + fn submit_dstack_attestation( + &mut self, + node_id: NodeId, + attestation: DstackAttestation, + ) -> Result { + let Some(verifier_account_id) = self.tee_verifier_account_id.clone() else { + return Err(TeeError::VerifierNotConfigured.into()); + }; + + Ok(Promise::new(verifier_account_id) + .function_call( + method_names::VERIFY_QUOTE.to_string(), + borsh::to_vec(&(&attestation.quote, &attestation.collateral)) + .expect("borsh serialization of verify_quote args must succeed"), + NearToken::from_near(0), + Gas::from_tgas(self.config.verifier_tera_gas), + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas::from_tgas(self.config.resolve_verification_tera_gas)) + .with_attached_deposit(env::attached_deposit()) + .resolve_verification(VerificationContext { + node_id, + attestation, + }), + )) } #[handle_result] @@ -1179,7 +1197,7 @@ impl MpcContract { .function_call( method_names::REMOVE_NON_PARTICIPANT_UPDATE_VOTES.to_string(), vec![], - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas(self.config.remove_non_participant_update_votes_tera_gas), ) .detach(); @@ -1188,7 +1206,7 @@ impl MpcContract { .function_call( method_names::CLEAN_TEE_STATUS.to_string(), vec![], - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas(self.config.clean_tee_status_tera_gas), ) .detach(); @@ -1200,7 +1218,7 @@ impl MpcContract { "max_scan": RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN })) .unwrap(), - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas(self.config.clean_invalid_attestations_tera_gas), ) .detach(); @@ -1209,7 +1227,7 @@ impl MpcContract { .function_call( method_names::CLEANUP_ORPHANED_NODE_MIGRATIONS.to_string(), vec![], - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas(self.config.cleanup_orphaned_node_migrations_tera_gas), ) .detach(); @@ -1218,7 +1236,7 @@ impl MpcContract { .function_call( method_names::CLEAN_FOREIGN_CHAIN_DATA.to_string(), vec![], - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas(self.config.clean_foreign_chain_data_tera_gas), ) .detach(); @@ -1227,7 +1245,7 @@ impl MpcContract { .function_call( method_names::REMOVE_NON_PARTICIPANT_TEE_VERIFIER_VOTES.to_string(), vec![], - NearToken::from_yoctonear(0), + NearToken::from_near(0), Gas::from_tgas( self.config .remove_non_participant_tee_verifier_votes_tera_gas, @@ -1325,10 +1343,8 @@ impl MpcContract { ); // Refund the difference if the proposer attached more than required. - if let Some(diff) = attached.checked_sub(required) - && diff > NearToken::from_yoctonear(0) - { - Promise::new(proposer).transfer(diff).detach(); + if let Some(diff) = attached.checked_sub(required) { + refund_to(&proposer, diff); } Ok(id) @@ -2271,6 +2287,83 @@ impl MpcContract { } } + /// Verify-quote callback: on a verifier verdict it runs the post-DCAP + /// checks and stores the attestation, refunding the flat fee if the + /// attestation is not accepted. + #[private] + #[payable] + pub fn resolve_verification( + &mut self, + #[serializer(borsh)] context: VerificationContext, + #[serializer(borsh)] + #[callback_result] + result: Result, + ) -> PromiseOrValue<()> { + let account_id = context.node_id.account_id.clone(); + log!("resolve_verification: account_id={account_id}"); + + let attestation_result = match result { + Ok(VerificationResult::Verified(report)) => { + self.verify_post_dcap_and_store(&context, &report) + } + Ok(VerificationResult::Rejected(reason)) => { + log!("verifier rejected quote for {account_id}: {reason}"); + Err(TeeError::QuoteRejected { + reason: reason.to_string(), + } + .into()) + } + // No verdict (verifier unreachable, panicked, or out of gas) + Err(promise_err) => { + log!("verifier did not answer for {account_id}: {promise_err:?}"); + Err(TeeError::VerifierUnavailable.into()) + } + }; + + match attestation_result { + Ok(()) => PromiseOrValue::Value(()), + Err(err) => { + refund_to(&account_id, env::attached_deposit()); + // Fail the submitter's transaction from a separate receipt so + // the refund 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(&err.to_string()) + .expect("borsh serialization of reason must succeed"), + NearToken::from_near(0), + Gas::from_tgas(self.config.fail_attestation_submission_tera_gas), + ); + PromiseOrValue::Promise(promise.as_return()) + } + } + } + + /// Runs the post-DCAP checks and stores the attestation for a + /// [`VerificationResult::Verified`] response. The deposit was already + /// checked against the flat fee in [`Self::submit_participant_info`], so this + /// only verifies and stores. + fn verify_post_dcap_and_store( + &mut self, + context: &VerificationContext, + report: &VerifiedReport, + ) -> Result<(), Error> { + let account_id = &context.node_id.account_id; + let tee_upgrade_deadline_duration = + Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds); + + if let Err(err) = self.tee_state.verify_and_store_dstack( + context.node_id.clone(), + &context.attestation, + report, + tee_upgrade_deadline_duration, + ) { + log!("post-DCAP check failed for {account_id}: {err}"); + return Err(err.into()); + } + + Ok(()) + } + /// Yield-resume callback for a single queued CKD request. /// /// On success, returns the confidential key to the original caller. On timeout, @@ -2337,6 +2430,12 @@ impl MpcContract { } } + #[private] + pub fn fail_attestation_submission(#[serializer(borsh)] reason: String) { + log!("fail_attestation_submission: {reason}"); + env::panic_str(&reason); + } + #[private] pub fn fail_on_timeout() { // To stay consistent with the old version of the timeout error @@ -2719,7 +2818,7 @@ mod tests { KeyProviderEventDigest, MrtdHash, Rtmr0Hash, Rtmr1Hash, Rtmr2Hash, }; use crate::tee::proposal::{LauncherVoteAction, get_docker_compose_hash}; - use crate::tee::tee_state::{NodeAttestation, NodeId}; + use crate::tee::tee_state::{NodeAttestation, NodeId, ParticipantInsertion}; use assert_matches::assert_matches; use dtos::{Attestation, Ed25519PublicKey, ForeignTxSignPayload, MockAttestation}; use dtos::{Curve, DomainConfig, DomainId, Payload, Protocol, ReconstructionThreshold, Tweak}; @@ -2727,9 +2826,9 @@ mod tests { use elliptic_curve::Group; use k256::{self, Secp256k1, ecdsa::SigningKey, elliptic_curve}; use mpc_attestation::attestation::{ - Attestation as MpcAttestation, MockAttestation as MpcMockAttestation, VerifiedAttestation, + MockAttestation as MpcMockAttestation, ValidatedDstackAttestation, VerifiedAttestation, + default_measurements, }; - use mpc_primitives::hash::DockerImageHash; use near_mpc_bounded_collections::{NonEmptyBTreeMap, NonEmptyBTreeSet}; use near_mpc_contract_interface::types::BackupServiceInfo; use near_mpc_contract_interface::types::CKDAppPublicKey; @@ -2747,10 +2846,6 @@ mod tests { use rstest::rstest; use sha2::{Digest, Sha256}; - use test_utils::attestation::{ - VALID_ATTESTATION_TIMESTAMP, image_digest, launcher_image_hash, - mock_dto_dstack_attestation, near_account_key, p2p_tls_key, - }; use test_utils::contract_types::dummy_config; use threshold_signatures::confidential_key_derivation as ckd; use threshold_signatures::frost_core::Group as _; @@ -3576,7 +3671,7 @@ mod tests { #[should_panic(expected = "Attached deposit is lower than required")] fn check_request_preconditions__panics_when_attached_deposit_is_insufficient() { let (_, contract, _) = basic_setup(Curve::Secp256k1, &mut OsRng); - override_context_for_preconditions(NearToken::from_yoctonear(0), Gas::from_tgas(300)); + override_context_for_preconditions(NearToken::from_near(0), Gas::from_tgas(300)); contract.check_request_preconditions( DomainId::default(), DomainPurpose::Sign, @@ -4023,7 +4118,9 @@ mod tests { .build(); testing_env!(participant_context); - contract.submit_participant_info(Attestation::Mock(attestation), dto_public_key) + contract + .submit_participant_info(Attestation::Mock(attestation), dto_public_key) + .map(|_| ()) } fn submit_valid_attestations( @@ -4052,7 +4149,7 @@ mod tests { let voting_context = VMContextBuilder::new() .signer_account_id(first_participant_id.clone()) .predecessor_account_id(first_participant_id.clone()) - .attached_deposit(NearToken::from_yoctonear(0)) + .attached_deposit(NearToken::from_near(0)) .build(); testing_env!(voting_context); @@ -4135,8 +4232,8 @@ mod tests { if let Err(error) = result { let error_string = error.to_string(); assert!( - error_string.contains("TeeQuoteStatus is invalid"), - "Error should mention invalid TEE status, got: {}", + error_string.contains("failed verification"), + "Error should mention attestation verification failure, got: {}", error_string ); } @@ -4209,7 +4306,7 @@ mod tests { VMContextBuilder::new() .signer_account_id(signer.clone()) .predecessor_account_id(signer.clone()) - .attached_deposit(NearToken::from_yoctonear(0)) + .attached_deposit(NearToken::from_near(0)) .build() ); contract.vote_new_parameters(EpochId::new(1), proposal.into_dto_type()) @@ -4350,7 +4447,7 @@ mod tests { let ctx = VMContextBuilder::new() .signer_account_id(first_participant_id) .predecessor_account_id("forwarder.near".parse().unwrap()) - .attached_deposit(NearToken::from_yoctonear(0)) + .attached_deposit(NearToken::from_near(0)) .build(); testing_env!(ctx); @@ -4440,7 +4537,7 @@ mod tests { .build(); testing_env!(ctx); - contract + let _ = contract .submit_participant_info(valid_attestation, participant_info.tls_public_key.clone()) .expect("Expected panic if predecessor != signer"); } @@ -4467,7 +4564,7 @@ mod tests { .build(); testing_env!(ctx); - contract + let _ = contract .submit_participant_info(valid_attestation, dto_public_key) .expect("Outsider attestation submission should succeed"); @@ -4529,7 +4626,7 @@ mod tests { .build() ); - contract + let _ = contract .submit_participant_info(Attestation::Mock(MockAttestation::Valid), dto_public_key) .unwrap(); @@ -5062,14 +5159,12 @@ mod tests { destination_node_info, ); } - let valid_participant_attestation = mpc_attestation::attestation::Attestation::Mock( - mpc_attestation::attestation::MockAttestation::Valid, - ); + let valid_participant_attestation = MpcMockAttestation::Valid; let tee_upgrade_duration = Duration::from_secs(contract.config.tee_upgrade_deadline_duration_seconds); - let insertion_result = contract.tee_state.add_participant( + let insertion_result = contract.tee_state.verify_and_store_mock( NodeId { account_id: self.signer_account_id.clone(), tls_public_key: self.attestation_tls_key.clone(), @@ -5719,15 +5814,15 @@ mod tests { // Replace the target's attestation with an expired one let node_id = create_node_id(target_account_id, &target_participant_info.tls_public_key); - let expiring_attestation = MpcAttestation::Mock(MpcMockAttestation::WithConstraints { + let expiring_attestation = MpcMockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(ATTESTATION_EXPIRY_SECONDS), expected_measurements: None, - }); + }; contract .tee_state - .add_participant(node_id, expiring_attestation, TEE_UPGRADE_DURATION) + .verify_and_store_mock(node_id, expiring_attestation, TEE_UPGRADE_DURATION) .expect("mock attestation is not yet expired and valid"); // Capture the running state before verify_tee for comparison @@ -5834,15 +5929,15 @@ mod tests { let (target_account_id, _, target_participant_info) = &participant_list[PARTICIPANT_COUNT - 1]; let node_id = create_node_id(target_account_id, &target_participant_info.tls_public_key); - let expiring_attestation = MpcAttestation::Mock(MpcMockAttestation::WithConstraints { + let expiring_attestation = MpcMockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(ATTESTATION_EXPIRY_SECONDS), expected_measurements: None, - }); + }; contract .tee_state - .add_participant(node_id, expiring_attestation, TEE_UPGRADE_DURATION) + .verify_and_store_mock(node_id, expiring_attestation, TEE_UPGRADE_DURATION) .expect("mock attestation is not yet expired and valid"); let (first_account_id, _, _) = &participant_list[0]; @@ -5865,247 +5960,6 @@ mod tests { assert!(!contract.accept_requests); } - /// Sets up a complete TEE test environment with contract, accounts, mock dstack attestation, TLS key and the node's near public key. - /// This is a helper function that provides all the common components needed for TEE-related tests. - fn setup_tee_test() -> ( - MpcContract, - Vec, - Attestation, - dtos::Ed25519PublicKey, - DockerImageHash, - near_sdk::PublicKey, - ) { - let (_context, contract, _secret_key) = basic_setup(Curve::Bls12381, &mut OsRng); - - let participant_account_ids: Vec<_> = contract - .protocol_state - .threshold_parameters() - .unwrap() - .participants() - .participants() - .iter() - .map(|(account_id, _, _)| account_id.clone()) - .collect(); - - let attestation = mock_dto_dstack_attestation(); - let tls_key = p2p_tls_key().into(); - let mpc_hash = image_digest(); - let near_public_key = near_account_key(); - - ( - contract, - participant_account_ids, - attestation, - tls_key, - mpc_hash, - near_public_key, - ) - } - - /// Sets up a contract with an approved MPC hash by having the participants vote for it. - /// Also adds the legacy launcher image hash so that compose hashes are derived correctly. - /// This is a helper function commonly used in tests that require pre-approved hashes. - fn setup_approved_mpc_hash( - contract: &mut MpcContract, - participant_account_ids: &[near_sdk::AccountId], - mpc_hash: &DockerImageHash, - block_timestamp_ns: u64, - ) { - // Add the legacy launcher image first, so that compose hashes are derived - // when the MPC hash is voted in. - setup_approved_launcher_hash(contract, participant_account_ids, block_timestamp_ns); - - for participant_account_id in participant_account_ids { - testing_env!( - VMContextBuilder::new() - .signer_account_id(participant_account_id.clone()) - .predecessor_account_id(participant_account_id.clone()) - .block_timestamp(block_timestamp_ns) - .build() - ); - - contract.vote_code_hash(*mpc_hash).expect("vote succeeds"); - } - } - - /// Adds the launcher image hash from test attestation assets. - /// The hash is extracted from `test-utils/assets/launcher_image_compose.yaml`. - fn setup_approved_launcher_hash( - contract: &mut MpcContract, - participant_account_ids: &[near_sdk::AccountId], - block_timestamp_ns: u64, - ) { - let launcher_hash = launcher_image_hash(); - - for participant_account_id in participant_account_ids { - testing_env!( - VMContextBuilder::new() - .signer_account_id(participant_account_id.clone()) - .predecessor_account_id(participant_account_id.clone()) - .block_timestamp(block_timestamp_ns) - .build() - ); - - contract - .vote_add_launcher_hash(launcher_hash) - .expect("launcher vote succeeds"); - } - } - - /// Adds the default OS measurements so that Dstack attestation verification passes. - fn setup_approved_measurements( - contract: &mut MpcContract, - participant_account_ids: &[near_sdk::AccountId], - block_timestamp_ns: u64, - ) { - for measurement in mpc_attestation::attestation::default_measurements() { - let contract_measurement = ContractExpectedMeasurements::from(*measurement); - for participant_account_id in participant_account_ids { - testing_env!( - VMContextBuilder::new() - .signer_account_id(participant_account_id.clone()) - .predecessor_account_id(participant_account_id.clone()) - .block_timestamp(block_timestamp_ns) - .build() - ); - - contract - .vote_add_os_measurement(contract_measurement.clone()) - .expect("measurement vote succeeds"); - } - } - } - - /// **Test method with matching measurements** - Tests that participant info submission succeeds with the test-only method. - /// Unlike the test above, this one has an approved MPC hash. It uses the test method with custom measurements that match - /// the attestation data. - #[test] - fn test_submit_participant_info_succeeds_with_valid_dstack_attestation() { - // given - let ( - mut contract, - participant_account_ids, - attestation, - tls_key, - mpc_hash, - near_public_key, - ) = setup_tee_test(); - - let block_timestamp_ns = VALID_ATTESTATION_TIMESTAMP * 1_000_000_000; - - // when - setup_approved_mpc_hash( - &mut contract, - &participant_account_ids, - &mpc_hash, - block_timestamp_ns, - ); - setup_approved_measurements(&mut contract, &participant_account_ids, block_timestamp_ns); - - let account_id = participant_account_ids[0].clone(); - testing_env!( - VMContextBuilder::new() - .signer_account_id(account_id.clone()) - .predecessor_account_id(account_id.clone()) - .signer_account_pk(near_public_key.clone()) - .attached_deposit(NearToken::from_near(1)) - .block_timestamp(block_timestamp_ns) - .build() - ); - let result = contract.submit_participant_info(attestation, tls_key); - - // then - assert_matches::assert_matches!(result, Ok(())); - } - - /// Note - this test uses attestation data from a real MPC node. After Any change to the expected contract measurement, /test-utils/assets need to be updated. - /// see crates/test-utils/assets/README.md for details. - /// **No MPC hash approval** - Tests that participant info submission fails when no MPC hash has been approved yet. - /// This verifies the prerequisite step: the contract requires MPC hash approval before accepting any participant TEE information. - #[test] - fn test_submit_participant_info_fails_without_approved_mpc_hash() { - // given - let ( - mut contract, - participant_account_ids, - attestation, - tls_key, - _mpc_hash, - near_public_key, - ) = setup_tee_test(); - - let block_timestamp_ns = VALID_ATTESTATION_TIMESTAMP * 1_000_000_000; - - // when - - let account_id = participant_account_ids[0].clone(); - testing_env!( - VMContextBuilder::new() - .signer_account_id(account_id.clone()) - .predecessor_account_id(account_id.clone()) - .signer_account_pk(near_public_key.clone()) - .attached_deposit(NearToken::from_near(1)) - .block_timestamp(block_timestamp_ns) - .build() - ); - let result = contract.submit_participant_info(attestation, tls_key); - - // then - let error_string = result.unwrap_err().to_string(); - assert!(error_string - .contains("Invalid TEE Remote Attestation: TeeQuoteStatus is invalid: the submitted attestation failed verification, reason: Custom(\"the allowed mpc image hashes list is empty\")"), "Got error: {}", error_string); - } - - /// **TLS key validation** - Tests that TEE attestation fails when TLS key doesn't match the one in report data. - /// Similar to the successful test method case above, but uses a deliberately corrupted TLS key to verify - /// that attestation validation properly checks the TLS key embedded in the attestation report. - #[test] - fn test_tee_attestation_fails_with_invalid_tls_key() { - let ( - mut contract, - participant_account_ids, - attestation, - tls_key, - mpc_hash, - near_public_key, - ) = setup_tee_test(); - - let block_timestamp_ns = VALID_ATTESTATION_TIMESTAMP * 1_000_000_000; - - // when - setup_approved_mpc_hash( - &mut contract, - &participant_account_ids, - &mpc_hash, - block_timestamp_ns, - ); - setup_approved_measurements(&mut contract, &participant_account_ids, block_timestamp_ns); - - // Create invalid TLS key by flipping the last bit - let mut invalid_tls_key_bytes = *tls_key.as_bytes(); - let last_byte_idx = invalid_tls_key_bytes.len() - 1; - invalid_tls_key_bytes[last_byte_idx] ^= 0x01; - let invalid_tls_key = Ed25519PublicKey::from(invalid_tls_key_bytes); - - let account_id = participant_account_ids[0].clone(); - testing_env!( - VMContextBuilder::new() - .signer_account_id(account_id.clone()) - .predecessor_account_id(account_id.clone()) - .signer_account_pk(near_public_key.clone()) - .attached_deposit(NearToken::from_near(1)) - .block_timestamp(block_timestamp_ns) - .build() - ); - - let result = contract.submit_participant_info(attestation, invalid_tls_key); - - // then - let error_string = result.unwrap_err().to_string(); - assert!(error_string - .contains("Invalid TEE Remote Attestation: TeeQuoteStatus is invalid: the submitted attestation failed verification, reason: WrongHash { name: \"report_data\""), "Got error: {}", error_string); - } - fn make_launcher_hash(byte: u8) -> LauncherImageHash { LauncherImageHash::from([byte; 32]) } @@ -7522,13 +7376,13 @@ mod tests { // Add attestation for the new node (mirrors what ConcludeNodeMigrationTestSetup::setup does). contract .tee_state - .add_participant( + .verify_and_store_mock( NodeId { account_id: operator4.clone(), tls_public_key: new_tls_key.clone(), account_public_key: new_signer_pk.clone(), }, - mpc_attestation::attestation::Attestation::Mock(MpcMockAttestation::Valid), + MpcMockAttestation::Valid, Duration::from_secs(contract.config.tee_upgrade_deadline_duration_seconds), ) .expect("attestation insertion should succeed"); @@ -8023,4 +7877,84 @@ mod tests { assert!(configs.contains_key(&tls_key_a), "node A config must exist"); assert!(configs.contains_key(&tls_key_b), "node B config must exist"); } + + #[test] + fn node_attestation__should_match_expected_max_borsh_size() { + // Given: the largest entry each variant can produce (64-byte account id, + // all fields present at their maximum). + let max_dstack_borsh_size = 445; + let max_mock_borsh_size = 450; + let node_id = create_node_id( + &"a".repeat(64).parse().unwrap(), + &bogus_ed25519_public_key(), + ); + let dstack = NodeAttestation { + node_id: node_id.clone(), + verified_attestation: VerifiedAttestation::Dstack(ValidatedDstackAttestation { + mpc_image_hash: [0xff; 32].into(), + launcher_compose_hash: [0xff; 32].into(), + expiry_timestamp_seconds: u64::MAX, + measurements: default_measurements()[0], + }), + }; + let mock = NodeAttestation { + node_id, + verified_attestation: VerifiedAttestation::Mock( + mpc_attestation::attestation::MockAttestation::WithConstraints { + mpc_docker_image_hash: Some([0xff; 32].into()), + launcher_docker_compose_hash: Some([0xff; 32].into()), + expiry_timestamp_seconds: Some(u64::MAX), + expected_measurements: Some(default_measurements()[0]), + }, + ), + }; + + // When + let dstack_size = borsh::to_vec(&dstack).unwrap().len(); + let mock_size = borsh::to_vec(&mock).unwrap().len(); + + // Then + assert_eq!(dstack_size, max_dstack_borsh_size); + assert_eq!(mock_size, max_mock_borsh_size); + } + + // Catches only entry-size growth: fails if a schema change makes the stored entry + // cost more than the fee at today's storage_byte_cost. It cannot see a future + // storage_byte_cost increase on a live contract; the fee's margin covers that. + #[test] + fn minimum_attestation_storage_deposit__should_cover_worst_case_entry() { + // Given: the largest entry a submission can store. NEAR caps an account id + // at 64 bytes; every other field is fixed-size, so this is the worst case. + testing_env!(VMContextBuilder::new().build()); + let node_id = create_node_id( + &"a".repeat(64).parse().unwrap(), + &bogus_ed25519_public_key(), + ); + let worst_case = NodeAttestation { + node_id: node_id.clone(), + verified_attestation: VerifiedAttestation::Dstack(ValidatedDstackAttestation { + mpc_image_hash: [0xff; 32].into(), + launcher_compose_hash: [0xff; 32].into(), + expiry_timestamp_seconds: u64::MAX, + measurements: default_measurements()[0], + }), + }; + + // When: the entry is inserted and flushed, so storage_usage reflects it. + let mut tee_state = TeeState::default(); + let storage_before = env::storage_usage(); + tee_state + .stored_attestations + .insert(node_id.tls_public_key.clone(), worst_case); + tee_state.stored_attestations.flush(); + let bytes_grown = env::storage_usage() - storage_before; + let worst_case_cost = env::storage_byte_cost().saturating_mul(u128::from(bytes_grown)); + + // Then: the flat fee covers the worst-case cost with headroom to spare. + assert!( + MINIMUM_ATTESTATION_STORAGE_DEPOSIT >= worst_case_cost, + "flat fee {MINIMUM_ATTESTATION_STORAGE_DEPOSIT} must cover the worst-case entry \ + ({bytes_grown} bytes, {worst_case_cost}) at today's storage price" + ); + } } diff --git a/crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap b/crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap index a92c342dd9..73ee9018ba 100644 --- a/crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap +++ b/crates/contract/src/snapshots/mpc_contract__tests__mpc_contract_borsh_schema_has_not_changed.snap @@ -210,6 +210,10 @@ BorshSchemaContainer { "fail_on_timeout_tera_gas", "u64", ), + ( + "fail_attestation_submission_tera_gas", + "u64", + ), ( "clean_tee_status_tera_gas", "u64", @@ -234,6 +238,14 @@ BorshSchemaContainer { "remove_non_participant_tee_verifier_votes_tera_gas", "u64", ), + ( + "verifier_tera_gas", + "u64", + ), + ( + "resolve_verification_tera_gas", + "u64", + ), ], ), }, diff --git a/crates/contract/src/tee.rs b/crates/contract/src/tee.rs index 9fafd439f9..00b42f33b4 100644 --- a/crates/contract/src/tee.rs +++ b/crates/contract/src/tee.rs @@ -3,4 +3,5 @@ pub mod proposal; pub mod tee_state; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; +pub mod verification_context; pub mod verifier_votes; diff --git a/crates/contract/src/tee/tee_state.rs b/crates/contract/src/tee/tee_state.rs index 2b0fffe928..c3fb05f0f8 100644 --- a/crates/contract/src/tee/tee_state.rs +++ b/crates/contract/src/tee/tee_state.rs @@ -11,13 +11,17 @@ use crate::{ }; use borsh::{BorshDeserialize, BorshSerialize}; use mpc_attestation::{ - attestation::{self, AcceptedAttestation, Attestation, VerifiedAttestation}, + attestation::{ + self, AcceptedAttestation, DstackAttestation, DstackVerify, MockAttestation, + VerifiedAttestation, + }, report_data::{ReportData, ReportDataV1}, }; use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash}; use near_mpc_contract_interface::types::{self as dtos, Ed25519PublicKey}; use near_sdk::{env, near, store::IterableMap}; use std::time::Duration; +use tee_verifier_interface::VerifiedReport; pub use near_mpc_contract_interface::types::NodeId; @@ -33,8 +37,8 @@ pub enum TeeQuoteStatus { Invalid(String), } -#[derive(Debug, Clone, thiserror::Error)] -pub(crate) enum AttestationSubmissionError { +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum AttestationSubmissionError { #[error("the submitted attestation failed verification, reason: {:?}", .0)] InvalidAttestation(#[from] attestation::VerificationError), #[error( @@ -143,31 +147,47 @@ impl TeeState { } fn current_time_seconds() -> u64 { - let current_time_milliseconds = env::block_timestamp_ms(); - current_time_milliseconds / 1_000 + env::block_timestamp_ms() / 1_000 } - /// Adds a participant attestation for the given node iff the attestation succeeds verification. - pub(crate) fn add_participant( + pub(crate) fn verify_and_store_mock( &mut self, node_id: NodeId, - attestation: Attestation, + mock: MockAttestation, tee_upgrade_deadline_duration: Duration, ) -> Result { - let expected_report_data: ReportData = ReportDataV1::new( - *node_id.tls_public_key.as_bytes(), - *node_id.account_public_key.as_bytes(), - ) - .into(); + let AcceptedAttestation { + attestation: verified_attestation, + advisory_ids, + } = mock.verify( + Self::current_time_seconds(), + &self.get_allowed_mpc_docker_image_hashes(tee_upgrade_deadline_duration), + &self.get_allowed_launcher_compose_hashes(), + &self.get_accepted_measurements(), + )?; + + log_informational_advisory_ids(&advisory_ids); + self.store_verified_attestation(node_id, verified_attestation) + } + + /// Runs the post-DCAP checks for a [`DstackAttestation`] against the + /// [`VerifiedReport`] the verifier returned, then stores the result. + pub(crate) fn verify_and_store_dstack( + &mut self, + node_id: NodeId, + dstack: &DstackAttestation, + report: &VerifiedReport, + tee_upgrade_deadline_duration: Duration, + ) -> Result { + let expected_report_data = Self::expected_report_data(&node_id); let accepted_measurements = self.get_accepted_measurements(); - // TODO(#3264): run DCAP in the verifier contract (Promise + callback) and - // do the post-DCAP checks here, instead of verifying locally in-WASM. let AcceptedAttestation { attestation: verified_attestation, advisory_ids, - } = attestation.verify_locally( - expected_report_data.into(), + } = dstack.verify( + report, + expected_report_data, Self::current_time_seconds(), &self.get_allowed_mpc_docker_image_hashes(tee_upgrade_deadline_duration), &self.get_allowed_launcher_compose_hashes(), @@ -175,7 +195,27 @@ impl TeeState { )?; log_informational_advisory_ids(&advisory_ids); + self.store_verified_attestation(node_id, verified_attestation) + } + + fn expected_report_data(node_id: &NodeId) -> ::attestation::report_data::ReportData { + let report_data: ReportData = ReportDataV1::new( + *node_id.tls_public_key.as_bytes(), + *node_id.account_public_key.as_bytes(), + ) + .into(); + report_data.into() + } + /// Stores `verified_attestation` under `node_id`'s TLS key, reporting whether the + /// entry was newly inserted or updated an existing one. Rejects a submission whose + /// TLS key is already registered to a different account with + /// [`AttestationSubmissionError::TlsKeyOwnedByOtherAccount`]. + fn store_verified_attestation( + &mut self, + node_id: NodeId, + verified_attestation: VerifiedAttestation, + ) -> Result { let tls_pk = node_id.tls_public_key.clone(); // Authorization: a TLS key registered to one account must not be @@ -188,7 +228,7 @@ impl TeeState { return Err(AttestationSubmissionError::TlsKeyOwnedByOtherAccount); } - let insertion = self.stored_attestations.insert( + let previous = self.stored_attestations.insert( tls_pk, NodeAttestation { node_id, @@ -196,8 +236,8 @@ impl TeeState { }, ); - Ok(match insertion { - Some(_previous_attestation) => ParticipantInsertion::UpdatedExistingParticipant, + Ok(match previous { + Some(_) => ParticipantInsertion::UpdatedExistingParticipant, None => ParticipantInsertion::NewlyInsertedParticipant, }) } @@ -541,7 +581,7 @@ mod tests { }; use crate::tee::test_utils::set_block_timestamp; use assert_matches::assert_matches; - use mpc_attestation::attestation::{Attestation, MockAttestation}; + use mpc_attestation::attestation::MockAttestation; use mpc_primitives::hash::{LauncherImageHash, NodeImageHash}; use near_account_id::AccountId; use near_sdk::test_utils::VMContextBuilder; @@ -576,13 +616,13 @@ mod tests { .collect(); // Add TEE information for all participants and non-participant - let local_attestation = Attestation::Mock(MockAttestation::Valid); + let local_attestation = MockAttestation::Valid; let non_participant_uid = node_id_for(&non_participant); for node_id in &participant_nodes { tee_state - .add_participant( + .verify_and_store_mock( node_id.clone(), local_attestation.clone(), TEE_UPGRADE_DURATION, @@ -590,7 +630,7 @@ mod tests { .unwrap(); } tee_state - .add_participant( + .verify_and_store_mock( non_participant_uid.clone(), local_attestation.clone(), TEE_UPGRADE_DURATION, @@ -633,24 +673,24 @@ mod tests { let fresh_node = node_id_for(&"fresh.near".parse().unwrap()); let stale_node = node_id_for(&"stale.near".parse().unwrap()); - let fresh = Attestation::Mock(MockAttestation::WithConstraints { + let fresh = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(FRESH_EXPIRY_SECONDS), expected_measurements: None, - }); - let stale = Attestation::Mock(MockAttestation::WithConstraints { + }; + let stale = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(STALE_EXPIRY_SECONDS), expected_measurements: None, - }); + }; tee_state - .add_participant(fresh_node.clone(), fresh, Duration::from_secs(0)) + .verify_and_store_mock(fresh_node.clone(), fresh, Duration::from_secs(0)) .unwrap(); tee_state - .add_participant(stale_node.clone(), stale, Duration::from_secs(0)) + .verify_and_store_mock(stale_node.clone(), stale, Duration::from_secs(0)) .unwrap(); assert_eq!(tee_state.stored_attestations.len(), 2); @@ -683,17 +723,17 @@ mod tests { let mut tee_state = TeeState::default(); - let expired = Attestation::Mock(MockAttestation::WithConstraints { + let expired = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(EXPIRY_SECONDS), expected_measurements: None, - }); + }; for idx in 0..10 { let node_id = node_id_for(&format!("node{idx}.near").parse().unwrap()); tee_state - .add_participant(node_id, expired.clone(), Duration::from_secs(0)) + .verify_and_store_mock(node_id, expired.clone(), Duration::from_secs(0)) .unwrap(); } assert_eq!(tee_state.stored_attestations.len(), 10); @@ -725,14 +765,14 @@ mod tests { let mut tee_state = TeeState::default(); let node_id = node_id_for(&"alice.near".parse().unwrap()); - let attestation = Attestation::Mock(MockAttestation::WithConstraints { + let attestation = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(FUTURE_EXPIRY_SECONDS), expected_measurements: None, - }); + }; tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // When: cleanup runs while the attestation is still valid. @@ -754,11 +794,11 @@ mod tests { let mut tee_state = TeeState::default(); let participant: AccountId = "dave.near".parse().unwrap(); - let local_attestation = Attestation::Mock(MockAttestation::Valid); + let local_attestation = MockAttestation::Valid; let participant_id = node_id_for(&participant); - let insertion_result = tee_state.add_participant( + let insertion_result = tee_state.verify_and_store_mock( participant_id.clone(), local_attestation.clone(), TEE_UPGRADE_DURATION, @@ -769,7 +809,7 @@ mod tests { ); // when - let re_insertion_result = tee_state.add_participant( + let re_insertion_result = tee_state.verify_and_store_mock( participant_id.clone(), local_attestation.clone(), TEE_UPGRADE_DURATION, @@ -783,15 +823,15 @@ mod tests { } #[test] - fn add_participant_increases_storage_size() { + fn verify_and_store_mock__should_increase_storage_size() { // given let mut tee_state = TeeState::default(); let node_id = node_id_for(&"alice.near".parse().unwrap()); - let attestation = Attestation::Mock(MockAttestation::Valid); + let attestation = MockAttestation::Valid; // when tee_state - .add_participant(node_id, attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id, attestation, Duration::from_secs(0)) .unwrap(); // then @@ -803,15 +843,15 @@ mod tests { } #[test] - fn add_participant_indexes_by_tls_key() { + fn verify_and_store_mock__should_index_by_tls_key() { // given let mut tee_state = TeeState::default(); let node_id = node_id_for(&"alice.near".parse().unwrap()); - let attestation = Attestation::Mock(MockAttestation::Valid); + let attestation = MockAttestation::Valid; // when tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // then @@ -824,15 +864,15 @@ mod tests { } #[test] - fn add_participant_preserves_node_id_integrity() { + fn verify_and_store_mock__should_preserve_node_id_integrity() { // given let mut tee_state = TeeState::default(); let node_id = node_id_for(&"alice.near".parse().unwrap()); - let attestation = Attestation::Mock(MockAttestation::Valid); + let attestation = MockAttestation::Valid; // when tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // then @@ -858,16 +898,16 @@ mod tests { // when tee_state - .add_participant( + .verify_and_store_mock( node_1.clone(), - Attestation::Mock(MockAttestation::Valid), + MockAttestation::Valid, Duration::from_secs(0), ) .unwrap(); tee_state - .add_participant( + .verify_and_store_mock( node_2.clone(), - Attestation::Mock(MockAttestation::Valid), + MockAttestation::Valid, Duration::from_secs(0), ) .unwrap(); @@ -896,15 +936,15 @@ mod tests { testing_env!(VMContextBuilder::new().block_timestamp(NOW_SECONDS).build()); - let attestation = Attestation::Mock(MockAttestation::WithConstraints { + let attestation = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(NOW_SECONDS), expected_measurements: None, - }); + }; tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // when @@ -925,15 +965,15 @@ mod tests { testing_env!(VMContextBuilder::new().block_timestamp(0).build()); - let attestation = Attestation::Mock(MockAttestation::WithConstraints { + let attestation = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(EXPIRY_TIMESTAMP_SECONDS), expected_measurements: None, - }); + }; tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // when @@ -966,15 +1006,15 @@ mod tests { .build() ); - let attestation = Attestation::Mock(MockAttestation::WithConstraints { + let attestation = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(EXPIRY_TIMESTAMP_SECONDS), expected_measurements: None, - }); + }; tee_state - .add_participant(node_id.clone(), attestation, Duration::from_secs(0)) + .verify_and_store_mock(node_id.clone(), attestation, Duration::from_secs(0)) .unwrap(); // when @@ -1023,11 +1063,7 @@ mod tests { account_public_key: Ed25519PublicKey::try_from(&signer_pk).unwrap(), }; tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("Attestation is valid on insertion"); // 4. Verify check passes @@ -1088,11 +1124,7 @@ mod tests { account_public_key: Ed25519PublicKey::try_from(&signer_pk).unwrap(), }; tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("Attestation is valid on insertion"); let result = tee_state.is_caller_an_attested_participant(&participants); @@ -1124,11 +1156,7 @@ mod tests { account_public_key: old_signer_pk, // Mismatch here }; tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("Attestation is valid on insertion"); // when @@ -1162,11 +1190,7 @@ mod tests { for (account_id, _, participant_info) in participants.participants().iter() { let node_id = create_node_id(account_id, &participant_info.tls_public_key); tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("mock attestation is valid"); } @@ -1187,11 +1211,7 @@ mod tests { for (account_id, _, participant_info) in participant_list.iter().take(2) { let node_id = create_node_id(account_id, &participant_info.tls_public_key); tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("mock attestation is valid"); } // Third participant has no attestation @@ -1221,25 +1241,21 @@ mod tests { for (account_id, _, participant_info) in participant_list.iter().take(2) { let node_id = create_node_id(account_id, &participant_info.tls_public_key); tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("mock attestation is valid"); } // Add expiring attestation for third participant let (account_id, _, participant_info) = &participant_list[2]; let node_id = create_node_id(account_id, &participant_info.tls_public_key); - let expiring_attestation = Attestation::Mock(MockAttestation::WithConstraints { + let expiring_attestation = MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(expiry_time_secs), expected_measurements: None, - }); + }; tee_state - .add_participant(node_id, expiring_attestation, tee_upgrade_duration) + .verify_and_store_mock(node_id, expiring_attestation, tee_upgrade_duration) .expect("mock attestation is valid"); // Advance time to exact expiry boundary @@ -1272,17 +1288,17 @@ mod tests { for (i, (account_id, _, participant_info)) in participant_list.iter().enumerate() { let node_id = create_node_id(account_id, &participant_info.tls_public_key); let attestation = if i == 2 { - Attestation::Mock(MockAttestation::WithConstraints { + MockAttestation::WithConstraints { mpc_docker_image_hash: None, launcher_docker_compose_hash: None, expiry_timestamp_seconds: Some(expiry_time_secs), expected_measurements: None, - }) + } } else { - Attestation::Mock(MockAttestation::Valid) + MockAttestation::Valid }; tee_state - .add_participant(node_id, attestation, tee_upgrade_duration) + .verify_and_store_mock(node_id, attestation, tee_upgrade_duration) .expect("mock attestation is valid"); } @@ -1300,7 +1316,7 @@ mod tests { } #[test] - fn add_participant__should_reject_tls_key_owned_by_other_account() { + fn verify_and_store_mock__should_reject_tls_key_owned_by_other_account() { // Given: an existing attestation registered to `alice.near` under some TLS key. const TEE_UPGRADE_DURATION: Duration = Duration::from_secs(10_000); @@ -1309,18 +1325,18 @@ mod tests { let alice_node = create_node_id(&"alice.near".parse().unwrap(), &tls_public_key); tee_state - .add_participant( + .verify_and_store_mock( alice_node.clone(), - Attestation::Mock(MockAttestation::Valid), + MockAttestation::Valid, TEE_UPGRADE_DURATION, ) .expect("initial insertion should succeed"); // When: a different account submits an attestation for the same TLS key. let attacker_node = create_node_id(&"attacker.near".parse().unwrap(), &tls_public_key); - let result = tee_state.add_participant( + let result = tee_state.verify_and_store_mock( attacker_node, - Attestation::Mock(MockAttestation::Valid), + MockAttestation::Valid, TEE_UPGRADE_DURATION, ); @@ -1337,7 +1353,7 @@ mod tests { } #[test] - fn add_participant__should_allow_same_account_to_update_its_own_entry() { + fn verify_and_store_mock__should_allow_same_account_to_update_its_own_entry() { // Given: an existing attestation registered to `alice.near`. const TEE_UPGRADE_DURATION: Duration = Duration::from_secs(10_000); @@ -1346,18 +1362,14 @@ mod tests { let initial_node = create_node_id(&"alice.near".parse().unwrap(), &tls_public_key); tee_state - .add_participant( - initial_node, - Attestation::Mock(MockAttestation::Valid), - TEE_UPGRADE_DURATION, - ) + .verify_and_store_mock(initial_node, MockAttestation::Valid, TEE_UPGRADE_DURATION) .expect("initial insertion should succeed"); // When: the same account resubmits with a rotated account_public_key. let rotated_node = create_node_id(&"alice.near".parse().unwrap(), &tls_public_key); - let result = tee_state.add_participant( + let result = tee_state.verify_and_store_mock( rotated_node.clone(), - Attestation::Mock(MockAttestation::Valid), + MockAttestation::Valid, TEE_UPGRADE_DURATION, ); @@ -1371,7 +1383,7 @@ mod tests { } #[test] - fn add_participant_rejects_invalid_attestations() { + fn verify_and_store_mock__should_reject_invalid_attestations() { let mut tee_state = TeeState::default(); let participants = gen_participants(3); let participant_list: Vec<_> = participants.participants().to_vec(); @@ -1381,20 +1393,16 @@ mod tests { for (account_id, _, participant_info) in participant_list.iter().take(2) { let node_id = create_node_id(account_id, &participant_info.tls_public_key); tee_state - .add_participant( - node_id, - Attestation::Mock(MockAttestation::Valid), - tee_upgrade_duration, - ) + .verify_and_store_mock(node_id, MockAttestation::Valid, tee_upgrade_duration) .expect("mock attestation is valid"); } // Add invalid attestation for third participant let (account_id, _, participant_info) = &participant_list[2]; let node_id = create_node_id(account_id, &participant_info.tls_public_key); - let add_participant_result = tee_state.add_participant( + let add_participant_result = tee_state.verify_and_store_mock( node_id, - Attestation::Mock(MockAttestation::Invalid), + MockAttestation::Invalid, tee_upgrade_duration, ); diff --git a/crates/contract/src/tee/verification_context.rs b/crates/contract/src/tee/verification_context.rs new file mode 100644 index 0000000000..6450f60c04 --- /dev/null +++ b/crates/contract/src/tee/verification_context.rs @@ -0,0 +1,14 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use mpc_attestation::attestation::DstackAttestation; + +use super::tee_state::NodeId; + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + all(feature = "abi", not(target_arch = "wasm32")), + derive(borsh::BorshSchema) +)] +pub struct VerificationContext { + pub(crate) node_id: NodeId, + pub(crate) attestation: DstackAttestation, +} diff --git a/crates/contract/src/v3_13_0_state.rs b/crates/contract/src/v3_13_0_state.rs index 83f727d076..9fc2fd10fd 100644 --- a/crates/contract/src/v3_13_0_state.rs +++ b/crates/contract/src/v3_13_0_state.rs @@ -15,7 +15,8 @@ use near_sdk::{ }; use crate::{ - Config, SupportedForeignChainsByNode, + SupportedForeignChainsByNode, + config::Config, foreign_chains_metadata::ForeignChainsMetadata, node_migrations::NodeMigrations, primitives::{ @@ -27,6 +28,60 @@ use crate::{ update::ProposedUpdates, }; +/// Shadow of the `3.13.0` [`Config`]: the deployed layout predates the async +/// attestation gas fields (`fail_attestation_submission_tera_gas`, +/// `verifier_tera_gas`, `resolve_verification_tera_gas`), so migrating state +/// written by `3.13.0` must deserialize the old field set and then default the +/// new ones. +#[derive(Debug, BorshSerialize, BorshDeserialize)] +struct OldConfig { + key_event_timeout_blocks: u64, + tee_upgrade_deadline_duration_seconds: u64, + contract_upgrade_deposit_tera_gas: u64, + sign_call_gas_attachment_requirement_tera_gas: u64, + ckd_call_gas_attachment_requirement_tera_gas: u64, + return_signature_and_clean_state_on_success_call_tera_gas: u64, + return_ck_and_clean_state_on_success_call_tera_gas: u64, + fail_on_timeout_tera_gas: u64, + clean_tee_status_tera_gas: u64, + clean_invalid_attestations_tera_gas: u64, + cleanup_orphaned_node_migrations_tera_gas: u64, + remove_non_participant_update_votes_tera_gas: u64, + clean_foreign_chain_data_tera_gas: u64, + remove_non_participant_tee_verifier_votes_tera_gas: u64, +} + +impl From for Config { + fn from(old: OldConfig) -> Self { + // Carry the deployed values; the async attestation gas fields are new in + // this release, so take their defaults. + Config { + key_event_timeout_blocks: old.key_event_timeout_blocks, + tee_upgrade_deadline_duration_seconds: old.tee_upgrade_deadline_duration_seconds, + contract_upgrade_deposit_tera_gas: old.contract_upgrade_deposit_tera_gas, + sign_call_gas_attachment_requirement_tera_gas: old + .sign_call_gas_attachment_requirement_tera_gas, + ckd_call_gas_attachment_requirement_tera_gas: old + .ckd_call_gas_attachment_requirement_tera_gas, + return_signature_and_clean_state_on_success_call_tera_gas: old + .return_signature_and_clean_state_on_success_call_tera_gas, + return_ck_and_clean_state_on_success_call_tera_gas: old + .return_ck_and_clean_state_on_success_call_tera_gas, + fail_on_timeout_tera_gas: old.fail_on_timeout_tera_gas, + clean_tee_status_tera_gas: old.clean_tee_status_tera_gas, + clean_invalid_attestations_tera_gas: old.clean_invalid_attestations_tera_gas, + cleanup_orphaned_node_migrations_tera_gas: old + .cleanup_orphaned_node_migrations_tera_gas, + remove_non_participant_update_votes_tera_gas: old + .remove_non_participant_update_votes_tera_gas, + clean_foreign_chain_data_tera_gas: old.clean_foreign_chain_data_tera_gas, + remove_non_participant_tee_verifier_votes_tera_gas: old + .remove_non_participant_tee_verifier_votes_tera_gas, + ..Config::default() + } + } +} + /// Keep this module in sync with [`crate::MpcContract`]: the moment a field's borsh /// layout diverges, shadow the old type here (see this module's history for examples) so /// state written by the `3.13.0` contract still deserializes during migration. @@ -38,7 +93,7 @@ pub struct MpcContract { pending_verify_foreign_tx_requests: LookupMap>, proposed_updates: ProposedUpdates, node_foreign_chain_support: SupportedForeignChainsByNode, - config: Config, + config: OldConfig, tee_state: TeeState, accept_requests: bool, node_migrations: NodeMigrations, @@ -61,7 +116,7 @@ impl From for crate::MpcContract { pending_verify_foreign_tx_requests: old.pending_verify_foreign_tx_requests, proposed_updates: old.proposed_updates, node_foreign_chain_support: old.node_foreign_chain_support, - config: old.config, + config: old.config.into(), tee_state: old.tee_state, accept_requests: old.accept_requests, node_migrations: old.node_migrations, diff --git a/crates/contract/tests/inprocess/attestation_submission.rs b/crates/contract/tests/inprocess/attestation_submission.rs index da5c7e6dd1..0d3d0fac17 100644 --- a/crates/contract/tests/inprocess/attestation_submission.rs +++ b/crates/contract/tests/inprocess/attestation_submission.rs @@ -3,17 +3,18 @@ use super::common; use mpc_contract::{ MpcContract, - errors::{Error, InvalidParameters}, + errors::Error, primitives::{ key_state::EpochId, participants::{ParticipantId, ParticipantInfo}, test_utils::{create_node_id, gen_participants, node_id_for}, thresholds::{ProposedThresholdParameters, Threshold, ThresholdParameters}, }, - tee::tee_state::NodeId, + tee::tee_state::{AttestationSubmissionError, NodeId}, }; -use near_mpc_contract_interface::types::{ - Attestation, InitConfig, MockAttestation, ProtocolContractState, +use near_mpc_contract_interface::{ + deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, + types::{Attestation, InitConfig, MockAttestation, ProtocolContractState}, }; use std::collections::BTreeMap; @@ -26,6 +27,9 @@ use std::time::Duration; const SECOND: Duration = Duration::from_secs(1); const NANOS_IN_SECOND: u64 = SECOND.as_nanos() as u64; +const ATTESTATION_STORAGE_DEPOSIT: NearToken = + NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR); + const DEFAULT_PARTICIPANT_COUNT: usize = 3; const DEFAULT_THRESHOLD_SIZE: u64 = 2; const DEFAUTL_CONTRACT_PROTOCOL_STATE: ContractProtocolState = ContractProtocolState::Running; @@ -145,9 +149,19 @@ impl TestSetup { node_id: &NodeId, attestation: Attestation, ) -> Result<(), mpc_contract::errors::Error> { - testing_env!(common::participant_context(&node_id.account_id)); + // `submit_participant_info` requires the flat storage fee, unlike the + // deposit-free calls `common::participant_context` is built for. + testing_env!( + VMContextBuilder::new() + .signer_account_id(node_id.account_id.clone()) + .predecessor_account_id(node_id.account_id.clone()) + .block_timestamp(near_sdk::env::block_timestamp()) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) + .build() + ); self.contract .submit_participant_info(attestation, node_id.tls_public_key.clone()) + .map(|_| ()) } /// Switches testing context to a given participant at a specific timestamp @@ -212,7 +226,7 @@ fn submit_participant_info__should_reject_overwrite_from_other_account() { testing_env!( VMContextBuilder::new() - .attached_deposit(NearToken::from_near(1)) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) .build() ); @@ -243,20 +257,24 @@ fn submit_participant_info__should_reject_overwrite_from_other_account() { VMContextBuilder::new() .signer_account_id(attacker_node.account_id.clone()) .predecessor_account_id(attacker_node.account_id.clone()) - .attached_deposit(NearToken::from_near(1)) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) .build() ); - let attack_result = setup.contract.submit_participant_info( - Attestation::Mock(MockAttestation::Valid), - attacker_node.tls_public_key.clone(), - ); + let attack_result = setup + .contract + .submit_participant_info( + Attestation::Mock(MockAttestation::Valid), + attacker_node.tls_public_key.clone(), + ) + .map(|_| ()); // Then: the contract rejects the call with the TLS-ownership error and the victim's // entry is unchanged. assert_matches!( &attack_result, - Err(Error::InvalidParameters(InvalidParameters::InvalidTeeRemoteAttestation { reason })) - if reason.contains("TLS public key is already registered") + Err(Error::AttestationSubmission( + AttestationSubmissionError::TlsKeyOwnedByOtherAccount + )) ); let stored_after = setup .contract @@ -277,7 +295,7 @@ fn clean_tee_status__should_not_touch_attestations() { testing_env!( VMContextBuilder::new() - .attached_deposit(NearToken::from_near(1)) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) .build() ); @@ -356,7 +374,7 @@ fn clean_invalid_attestations__should_remove_expired_entries() { testing_env!( VMContextBuilder::new() - .attached_deposit(NearToken::from_near(1)) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) .block_timestamp(0) .build() ); @@ -410,7 +428,7 @@ fn clean_invalid_attestations__should_reject_when_not_running() { // Given: contract sitting in Initializing state. testing_env!( VMContextBuilder::new() - .attached_deposit(NearToken::from_near(1)) + .attached_deposit(ATTESTATION_STORAGE_DEPOSIT) .block_timestamp(0) .build() ); diff --git a/crates/contract/tests/sandbox/contract_configuration.rs b/crates/contract/tests/sandbox/contract_configuration.rs index 83e370a2f0..b33c8db8cb 100644 --- a/crates/contract/tests/sandbox/contract_configuration.rs +++ b/crates/contract/tests/sandbox/contract_configuration.rs @@ -97,12 +97,15 @@ async fn contract_configuration_can_be_set_on_initialization() { return_signature_and_clean_state_on_success_call_tera_gas: Some(66), return_ck_and_clean_state_on_success_call_tera_gas: Some(77), fail_on_timeout_tera_gas: Some(88), + fail_attestation_submission_tera_gas: Some(89), clean_tee_status_tera_gas: Some(99), clean_invalid_attestations_tera_gas: Some(101), cleanup_orphaned_node_migrations_tera_gas: Some(11), remove_non_participant_update_votes_tera_gas: Some(12), clean_foreign_chain_data_tera_gas: Some(13), remove_non_participant_tee_verifier_votes_tera_gas: Some(14), + verifier_tera_gas: Some(15), + resolve_verification_tera_gas: Some(16), }; let SandboxTestSetup { contract, .. } = SandboxTestSetup::builder() diff --git a/crates/contract/tests/sandbox/tee.rs b/crates/contract/tests/sandbox/tee.rs index 1ab3d32a7d..d28ab3541d 100644 --- a/crates/contract/tests/sandbox/tee.rs +++ b/crates/contract/tests/sandbox/tee.rs @@ -3,7 +3,7 @@ use crate::sandbox::{ common::{SandboxTestSetup, build_sandbox_node_ids, gen_accounts, submit_tee_attestations}, utils::{ - consts::ALL_PROTOCOLS, + consts::{ALL_PROTOCOLS, SUBMIT_PARTICIPANT_INFO_DEPOSIT}, interface::IntoContractType, mpc_contract::{ assert_running_return_participants, assert_running_return_threshold, @@ -984,3 +984,104 @@ async fn verify_tee__should_keep_participants_and_stop_signing_when_kickout_drop Ok(()) } + +/// A submission attaching less than the flat storage fee is rejected before the +/// entry is stored. +#[tokio::test] +async fn submit_participant_info__should_reject_new_attestation_below_flat_fee() -> Result<()> { + // Given + let SandboxTestSetup { + worker, contract, .. + } = SandboxTestSetup::builder() + .with_protocols(ALL_PROTOCOLS) + .build() + .await; + let outsider = worker.dev_create_account().await?; + let fresh_tls_key = bogus_ed25519_public_key(); + let storage_before = worker.view_account(contract.id()).await?.storage_usage; + let below_fee = SUBMIT_PARTICIPANT_INFO_DEPOSIT.saturating_sub(NearToken::from_yoctonear(1)); + + // When + let result = outsider + .call(contract.id(), method_names::SUBMIT_PARTICIPANT_INFO) + .args_json(( + Attestation::Mock(MockAttestation::Valid), + fresh_tls_key.clone(), + )) + .deposit(below_fee) + .max_gas() + .transact() + .await?; + + // Then + assert!( + !result.is_success(), + "submission below the flat fee must fail: {result:?}" + ); + let error_msg = format!("{:?}", result.into_result()); + assert!( + error_msg.contains("Attached deposit is lower than required"), + "expected an insufficient-deposit error, got: {error_msg}" + ); + let stored = get_participant_attestation(&contract, &fresh_tls_key).await?; + assert!( + stored.is_none(), + "no attestation should be stored when the deposit is rejected" + ); + let storage_after = worker.view_account(contract.id()).await?.storage_usage; + assert_eq!( + storage_after, storage_before, + "contract storage must not grow when the submission is rejected" + ); + Ok(()) +} + +/// A submission attaching exactly the flat fee is stored, and the caller is +/// charged the whole fee with no excess refunded (the fee far exceeds the true +/// storage cost by design). +#[tokio::test] +async fn submit_participant_info__should_store_new_attestation_and_charge_the_flat_fee() +-> Result<()> { + // Given + let SandboxTestSetup { + worker, contract, .. + } = SandboxTestSetup::builder() + .with_protocols(ALL_PROTOCOLS) + .build() + .await; + let outsider = worker.dev_create_account().await?; + let fresh_tls_key = bogus_ed25519_public_key(); + let balance_before = outsider.view_account().await?.balance; + + // When + let result = outsider + .call(contract.id(), method_names::SUBMIT_PARTICIPANT_INFO) + .args_json(( + Attestation::Mock(MockAttestation::Valid), + fresh_tls_key.clone(), + )) + .deposit(SUBMIT_PARTICIPANT_INFO_DEPOSIT) + .max_gas() + .transact() + .await?; + + // Then + assert!( + result.is_success(), + "submission attaching the flat fee should succeed: {result:?}" + ); + let stored = get_participant_attestation(&contract, &fresh_tls_key).await?; + assert!( + stored.is_some(), + "the attestation entry should be stored on-chain" + ); + // The whole flat fee is consumed (no excess refund); `spent` also covers gas, + // so it must be at least the fee. + let balance_after = outsider.view_account().await?.balance; + let spent = balance_before.saturating_sub(balance_after); + assert!( + spent >= SUBMIT_PARTICIPANT_INFO_DEPOSIT, + "caller must be charged the full flat fee ({SUBMIT_PARTICIPANT_INFO_DEPOSIT}), spent {spent}" + ); + Ok(()) +} diff --git a/crates/contract/tests/sandbox/upgrade_from_current_contract.rs b/crates/contract/tests/sandbox/upgrade_from_current_contract.rs index e7b6f1be12..913b36ba73 100644 --- a/crates/contract/tests/sandbox/upgrade_from_current_contract.rs +++ b/crates/contract/tests/sandbox/upgrade_from_current_contract.rs @@ -113,12 +113,15 @@ async fn test_propose_update_config() { return_signature_and_clean_state_on_success_call_tera_gas: 66, return_ck_and_clean_state_on_success_call_tera_gas: 77, fail_on_timeout_tera_gas: 88, + fail_attestation_submission_tera_gas: 89, clean_tee_status_tera_gas: 99, clean_invalid_attestations_tera_gas: 101, cleanup_orphaned_node_migrations_tera_gas: 11, remove_non_participant_update_votes_tera_gas: 12, clean_foreign_chain_data_tera_gas: 13, remove_non_participant_tee_verifier_votes_tera_gas: 14, + verifier_tera_gas: 15, + resolve_verification_tera_gas: 16, }; let mut proposals = Vec::with_capacity(mpc_signer_accounts.len()); diff --git a/crates/contract/tests/sandbox/utils/consts.rs b/crates/contract/tests/sandbox/utils/consts.rs index b122dad2f6..bd9d862484 100644 --- a/crates/contract/tests/sandbox/utils/consts.rs +++ b/crates/contract/tests/sandbox/utils/consts.rs @@ -1,6 +1,8 @@ use std::time::Duration; -use near_mpc_contract_interface::types::Protocol; +use near_mpc_contract_interface::{ + deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, types::Protocol, +}; use near_sdk::{Gas, NearToken}; /* --- Protocol defaults --- */ @@ -45,4 +47,9 @@ pub const MAX_GAS_FOR_THRESHOLD_VOTE: Gas = Gas::from_tgas(190); /// TODO(#2756): Reduce this to the minimal value possible pub const CURRENT_CONTRACT_DEPLOY_DEPOSIT: NearToken = NearToken::from_millinear(17000); +/// Attached to `submit_participant_info`; the contract requires exactly this flat +/// fee to store the bounded attestation entry, with no refund. +pub const SUBMIT_PARTICIPANT_INFO_DEPOSIT: NearToken = + NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR); + pub const DEFAULT_MAX_TIMEOUT_TX_INCLUDED: Duration = Duration::from_secs(3); diff --git a/crates/contract/tests/sandbox/utils/mpc_contract.rs b/crates/contract/tests/sandbox/utils/mpc_contract.rs index ce9690131e..8e5aa06c80 100644 --- a/crates/contract/tests/sandbox/utils/mpc_contract.rs +++ b/crates/contract/tests/sandbox/utils/mpc_contract.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use super::consts::SUBMIT_PARTICIPANT_INFO_DEPOSIT; use super::transactions::all_receipts_successful; use mpc_contract::tee::tee_state::NodeId; use mpc_primitives::hash::{LauncherImageHash, NodeImageHash}; @@ -50,6 +51,7 @@ pub async fn submit_participant_info( let result = account .call(contract.id(), method_names::SUBMIT_PARTICIPANT_INFO) .args_json((attestation, tls_key)) + .deposit(SUBMIT_PARTICIPANT_INFO_DEPOSIT) .max_gas() .transact() .await?; diff --git a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap index 769ec60982..830f9c1120 100644 --- a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap +++ b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap @@ -566,6 +566,39 @@ expression: abi } } }, + { + "name": "fail_attestation_submission", + "kind": "view", + "modifiers": [ + "private" + ], + "params": { + "serialization_type": "borsh", + "args": [ + { + "name": "reason", + "type_schema": { + "declaration": "String", + "definitions": { + "String": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "u8" + } + }, + "u8": { + "Primitive": 1 + } + } + } + } + ] + } + }, { "name": "fail_on_timeout", "kind": "view", @@ -991,6 +1024,10 @@ expression: abi "fail_on_timeout_tera_gas", "u64" ], + [ + "fail_attestation_submission_tera_gas", + "u64" + ], [ "clean_tee_status_tera_gas", "u64" @@ -1014,6 +1051,14 @@ expression: abi [ "remove_non_participant_tee_verifier_votes_tera_gas", "u64" + ], + [ + "verifier_tera_gas", + "u64" + ], + [ + "resolve_verification_tera_gas", + "u64" ] ] }, @@ -1267,6 +1312,753 @@ expression: abi ] } }, + { + "name": "resolve_verification", + "doc": " Verify-quote callback: on a verifier verdict it runs the post-DCAP\n checks and stores the attestation, refunding the flat fee if the\n attestation is not accepted.", + "kind": "call", + "modifiers": [ + "payable", + "private" + ], + "params": { + "serialization_type": "borsh", + "args": [ + { + "name": "context", + "type_schema": { + "declaration": "VerificationContext", + "definitions": { + "()": { + "Primitive": 0 + }, + "AccountId": { + "Struct": [ + "String" + ] + }, + "Collateral": { + "Struct": [ + [ + "pck_crl_issuer_chain", + "String" + ], + [ + "root_ca_crl", + "Vec" + ], + [ + "pck_crl", + "Vec" + ], + [ + "tcb_info_issuer_chain", + "String" + ], + [ + "tcb_info", + "String" + ], + [ + "tcb_info_signature", + "Vec" + ], + [ + "qe_identity_issuer_chain", + "String" + ], + [ + "qe_identity", + "String" + ], + [ + "qe_identity_signature", + "Vec" + ], + [ + "pck_certificate_chain", + "Option" + ] + ] + }, + "DstackAttestation": { + "Struct": [ + [ + "quote", + "QuoteBytes" + ], + [ + "collateral", + "Collateral" + ], + [ + "tcb_info", + "TcbInfo" + ] + ] + }, + "Ed25519PublicKey": { + "Struct": [ + "[u8; 32]" + ] + }, + "EventLog": { + "Struct": [ + [ + "imr", + "u32" + ], + [ + "event_type", + "u32" + ], + [ + "digest", + "HexBytes<48>" + ], + [ + "event", + "String" + ], + [ + "event_payload", + "String" + ] + ] + }, + "HexBytes<32>": { + "Struct": [ + "[u8; 32]" + ] + }, + "HexBytes<48>": { + "Struct": [ + "[u8; 48]" + ] + }, + "NodeId": { + "Struct": [ + [ + "account_id", + "AccountId" + ], + [ + "tls_public_key", + "Ed25519PublicKey" + ], + [ + "account_public_key", + "Ed25519PublicKey" + ] + ] + }, + "Option>": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "None", + "()" + ], + [ + 1, + "Some", + "HexBytes<32>" + ] + ] + } + }, + "Option": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "None", + "()" + ], + [ + 1, + "Some", + "String" + ] + ] + } + }, + "QuoteBytes": { + "Struct": [ + "Vec" + ] + }, + "String": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "u8" + } + }, + "TcbInfo": { + "Struct": [ + [ + "mrtd", + "HexBytes<48>" + ], + [ + "rtmr0", + "HexBytes<48>" + ], + [ + "rtmr1", + "HexBytes<48>" + ], + [ + "rtmr2", + "HexBytes<48>" + ], + [ + "rtmr3", + "HexBytes<48>" + ], + [ + "os_image_hash", + "Option>" + ], + [ + "compose_hash", + "HexBytes<32>" + ], + [ + "device_id", + "HexBytes<32>" + ], + [ + "app_compose", + "String" + ], + [ + "event_log", + "Vec" + ] + ] + }, + "Vec": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "EventLog" + } + }, + "Vec": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "u8" + } + }, + "VerificationContext": { + "Struct": [ + [ + "node_id", + "NodeId" + ], + [ + "attestation", + "DstackAttestation" + ] + ] + }, + "[u8; 32]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 32, + "end": 32 + }, + "elements": "u8" + } + }, + "[u8; 48]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 48, + "end": 48 + }, + "elements": "u8" + } + }, + "u32": { + "Primitive": 4 + }, + "u8": { + "Primitive": 1 + } + } + } + } + ] + }, + "callbacks": [ + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "VerificationResult", + "definitions": { + "EnclaveReport": { + "Struct": [ + [ + "cpu_svn", + "[u8; 16]" + ], + [ + "misc_select", + "u32" + ], + [ + "reserved1", + "[u8; 28]" + ], + [ + "attributes", + "[u8; 16]" + ], + [ + "mr_enclave", + "[u8; 32]" + ], + [ + "reserved2", + "[u8; 32]" + ], + [ + "mr_signer", + "[u8; 32]" + ], + [ + "reserved3", + "[u8; 96]" + ], + [ + "isv_prod_id", + "u16" + ], + [ + "isv_svn", + "u16" + ], + [ + "reserved4", + "[u8; 60]" + ], + [ + "report_data", + "[u8; 64]" + ] + ] + }, + "Report": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "SgxEnclave", + "Report__SgxEnclave" + ], + [ + 1, + "TD10", + "Report__TD10" + ], + [ + 2, + "TD15", + "Report__TD15" + ] + ] + } + }, + "Report__SgxEnclave": { + "Struct": [ + "EnclaveReport" + ] + }, + "Report__TD10": { + "Struct": [ + "TDReport10" + ] + }, + "Report__TD15": { + "Struct": [ + "TDReport15" + ] + }, + "String": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "u8" + } + }, + "TDReport10": { + "Struct": [ + [ + "tee_tcb_svn", + "[u8; 16]" + ], + [ + "mr_seam", + "[u8; 48]" + ], + [ + "mr_signer_seam", + "[u8; 48]" + ], + [ + "seam_attributes", + "[u8; 8]" + ], + [ + "td_attributes", + "[u8; 8]" + ], + [ + "xfam", + "[u8; 8]" + ], + [ + "mr_td", + "[u8; 48]" + ], + [ + "mr_config_id", + "[u8; 48]" + ], + [ + "mr_owner", + "[u8; 48]" + ], + [ + "mr_owner_config", + "[u8; 48]" + ], + [ + "rt_mr0", + "[u8; 48]" + ], + [ + "rt_mr1", + "[u8; 48]" + ], + [ + "rt_mr2", + "[u8; 48]" + ], + [ + "rt_mr3", + "[u8; 48]" + ], + [ + "report_data", + "[u8; 64]" + ] + ] + }, + "TDReport15": { + "Struct": [ + [ + "base", + "TDReport10" + ], + [ + "tee_tcb_svn2", + "[u8; 16]" + ], + [ + "mr_service_td", + "[u8; 48]" + ] + ] + }, + "TcbStatus": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "UpToDate", + "TcbStatus__UpToDate" + ], + [ + 1, + "OutOfDateConfigurationNeeded", + "TcbStatus__OutOfDateConfigurationNeeded" + ], + [ + 2, + "OutOfDate", + "TcbStatus__OutOfDate" + ], + [ + 3, + "ConfigurationAndSWHardeningNeeded", + "TcbStatus__ConfigurationAndSWHardeningNeeded" + ], + [ + 4, + "ConfigurationNeeded", + "TcbStatus__ConfigurationNeeded" + ], + [ + 5, + "SWHardeningNeeded", + "TcbStatus__SWHardeningNeeded" + ], + [ + 6, + "Revoked", + "TcbStatus__Revoked" + ] + ] + } + }, + "TcbStatusWithAdvisory": { + "Struct": [ + [ + "status", + "TcbStatus" + ], + [ + "advisory_ids", + "Vec" + ] + ] + }, + "TcbStatus__ConfigurationAndSWHardeningNeeded": { + "Struct": null + }, + "TcbStatus__ConfigurationNeeded": { + "Struct": null + }, + "TcbStatus__OutOfDate": { + "Struct": null + }, + "TcbStatus__OutOfDateConfigurationNeeded": { + "Struct": null + }, + "TcbStatus__Revoked": { + "Struct": null + }, + "TcbStatus__SWHardeningNeeded": { + "Struct": null + }, + "TcbStatus__UpToDate": { + "Struct": null + }, + "Vec": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "String" + } + }, + "Vec": { + "Sequence": { + "length_width": 4, + "length_range": { + "start": 0, + "end": 4294967295 + }, + "elements": "u8" + } + }, + "VerificationResult": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "Verified", + "VerificationResult__Verified" + ], + [ + 1, + "Rejected", + "VerificationResult__Rejected" + ] + ] + } + }, + "VerificationResult__Rejected": { + "Struct": [ + "VerifierError" + ] + }, + "VerificationResult__Verified": { + "Struct": [ + "VerifiedReport" + ] + }, + "VerifiedReport": { + "Struct": [ + [ + "status", + "String" + ], + [ + "advisory_ids", + "Vec" + ], + [ + "report", + "Report" + ], + [ + "ppid", + "Vec" + ], + [ + "qe_status", + "TcbStatusWithAdvisory" + ], + [ + "platform_status", + "TcbStatusWithAdvisory" + ] + ] + }, + "VerifierError": { + "Enum": { + "tag_width": 1, + "variants": [ + [ + 0, + "DcapVerification", + "VerifierError__DcapVerification" + ] + ] + } + }, + "VerifierError__DcapVerification": { + "Struct": [ + "String" + ] + }, + "[u8; 16]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 16, + "end": 16 + }, + "elements": "u8" + } + }, + "[u8; 28]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 28, + "end": 28 + }, + "elements": "u8" + } + }, + "[u8; 32]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 32, + "end": 32 + }, + "elements": "u8" + } + }, + "[u8; 48]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 48, + "end": 48 + }, + "elements": "u8" + } + }, + "[u8; 60]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 60, + "end": 60 + }, + "elements": "u8" + } + }, + "[u8; 64]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 64, + "end": 64 + }, + "elements": "u8" + } + }, + "[u8; 8]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 8, + "end": 8 + }, + "elements": "u8" + } + }, + "[u8; 96]": { + "Sequence": { + "length_width": 0, + "length_range": { + "start": 96, + "end": 96 + }, + "elements": "u8" + } + }, + "u16": { + "Primitive": 2 + }, + "u32": { + "Primitive": 4 + }, + "u8": { + "Primitive": 1 + } + } + } + } + ], + "result": { + "serialization_type": "json", + "type_schema": { + "$ref": "#/definitions/PromiseOrValueNull" + } + } + }, { "name": "respond", "kind": "call", @@ -1544,7 +2336,7 @@ expression: abi }, { "name": "submit_participant_info", - "doc": " (Prospective) Participants can submit their tee participant information through this\n endpoint.", + "doc": " Submit a TEE attestation for a current or prospective participant.\n\n - [`Attestation::Mock`] is verified synchronously.\n - [`Attestation::Dstack`] is verified asynchronously via a cross-contract\n `verify_quote` call, with [`Self::resolve_verification`] chained as its\n callback to run the post-DCAP checks and store the attestation.\n\n The caller must attach a flat 0.1 NEAR fee for the stored entry; the whole\n fee is kept on success and refunded if the attestation is not accepted.", "kind": "call", "modifiers": [ "payable" @@ -1569,7 +2361,7 @@ expression: abi "result": { "serialization_type": "json", "type_schema": { - "type": "null" + "$ref": "#/definitions/PromiseOrValueNull" } } }, @@ -2772,14 +3564,17 @@ expression: abi "clean_tee_status_tera_gas", "cleanup_orphaned_node_migrations_tera_gas", "contract_upgrade_deposit_tera_gas", + "fail_attestation_submission_tera_gas", "fail_on_timeout_tera_gas", "key_event_timeout_blocks", "remove_non_participant_tee_verifier_votes_tera_gas", "remove_non_participant_update_votes_tera_gas", + "resolve_verification_tera_gas", "return_ck_and_clean_state_on_success_call_tera_gas", "return_signature_and_clean_state_on_success_call_tera_gas", "sign_call_gas_attachment_requirement_tera_gas", - "tee_upgrade_deadline_duration_seconds" + "tee_upgrade_deadline_duration_seconds", + "verifier_tera_gas" ], "properties": { "ckd_call_gas_attachment_requirement_tera_gas": { @@ -2818,6 +3613,12 @@ expression: abi "format": "uint64", "minimum": 0.0 }, + "fail_attestation_submission_tera_gas": { + "description": "Prepaid gas for a `fail_attestation_submission` call.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "fail_on_timeout_tera_gas": { "description": "Prepaid gas for a `fail_on_timeout` call.", "type": "integer", @@ -2842,6 +3643,12 @@ expression: abi "format": "uint64", "minimum": 0.0 }, + "resolve_verification_tera_gas": { + "description": "Prepaid gas for the `resolve_verification` callback.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "return_ck_and_clean_state_on_success_call_tera_gas": { "description": "Prepaid gas for a `return_ck_and_clean_state_on_success` call.", "type": "integer", @@ -2865,6 +3672,12 @@ expression: abi "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "verifier_tera_gas": { + "description": "Gas attached to the cross-contract `verify_quote` call on the verifier.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 } } }, @@ -3445,6 +4258,15 @@ expression: abi "format": "uint64", "minimum": 0.0 }, + "fail_attestation_submission_tera_gas": { + "description": "Prepaid gas for a `fail_attestation_submission` call.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "fail_on_timeout_tera_gas": { "description": "Prepaid gas for a `fail_on_timeout` call.", "type": [ @@ -3481,6 +4303,15 @@ expression: abi "format": "uint64", "minimum": 0.0 }, + "resolve_verification_tera_gas": { + "description": "Prepaid gas for the `resolve_verification` callback.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "return_ck_and_clean_state_on_success_call_tera_gas": { "description": "Prepaid gas for a `return_ck_and_clean_state_on_success` call.", "type": [ @@ -3516,6 +4347,15 @@ expression: abi ], "format": "uint64", "minimum": 0.0 + }, + "verifier_tera_gas": { + "description": "Gas attached to the cross-contract `verify_quote` call on the verifier.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } } }, @@ -4122,6 +4962,9 @@ expression: abi } } }, + "PromiseOrValueNull": { + "type": "null" + }, "PromiseOrValueSignatureResponse": { "oneOf": [ { diff --git a/crates/e2e-tests/src/cluster.rs b/crates/e2e-tests/src/cluster.rs index 524a52fbe5..8c5afa75e1 100644 --- a/crates/e2e-tests/src/cluster.rs +++ b/crates/e2e-tests/src/cluster.rs @@ -6,12 +6,15 @@ use anyhow::Context; use backon::{ConstantBuilder, Retryable}; use ed25519_dalek::SigningKey; use near_kit::AccountId; -use near_mpc_contract_interface::method_names; -use near_mpc_contract_interface::types::{ - AccountId as ContractAccountId, CKDAppPublicKey, DomainConfig, DomainId, DomainPurpose, - Ed25519PublicKey, EpochId, ParticipantId, ParticipantInfo, Participants, ProposeUpdateArgs, - ProposedThresholdParameters, Protocol, ProtocolContractState, ReconstructionThreshold, - Threshold, ThresholdParameters, +use near_mpc_contract_interface::{ + deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, + method_names, + types::{ + AccountId as ContractAccountId, CKDAppPublicKey, DomainConfig, DomainId, DomainPurpose, + Ed25519PublicKey, EpochId, ParticipantId, ParticipantInfo, Participants, ProposeUpdateArgs, + ProposedThresholdParameters, Protocol, ProtocolContractState, ReconstructionThreshold, + Threshold, ThresholdParameters, + }, }; use rand::SeedableRng; use rand::rngs::StdRng; @@ -46,6 +49,9 @@ const SIGN_DEPOSIT: near_kit::NearToken = near_kit::NearToken::from_yoctonear(1) const KEY_EVENT_TIMEOUT_BLOCKS: u64 = 240; const CONTRACT_UPDATE_DEPOSIT: near_kit::NearToken = near_kit::NearToken::from_millinear(17_000); const CONTRACT_UPDATE_GAS: near_kit::Gas = near_kit::Gas::from_tgas(300); +const SUBMIT_PARTICIPANT_INFO_DEPOSIT: near_kit::NearToken = + near_kit::NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR); +const SUBMIT_PARTICIPANT_INFO_GAS: near_kit::Gas = near_kit::Gas::from_tgas(300); const CONTRACT_DEPLOY_TIMEOUT: Duration = Duration::from_secs(15); const PROPOSER_NODE_INDEX: usize = 0; @@ -1202,13 +1208,15 @@ async fn init_contract( let pubkey = near_mpc_crypto_types::Ed25519PublicKey::from(p2p_keys[i].verifying_key().to_bytes()); contract - .call_from( + .call_from_with_deposit( &client, method_names::SUBMIT_PARTICIPANT_INFO, json!({ "proposed_participant_attestation": { "Mock": "Valid" }, "tls_public_key": pubkey, }), + SUBMIT_PARTICIPANT_INFO_GAS, + SUBMIT_PARTICIPANT_INFO_DEPOSIT, ) .await .with_context(|| format!("failed to submit attestation for node {i}"))?; diff --git a/crates/mpc-attestation/src/attestation.rs b/crates/mpc-attestation/src/attestation.rs index b946e00d4d..752496fdb2 100644 --- a/crates/mpc-attestation/src/attestation.rs +++ b/crates/mpc-attestation/src/attestation.rs @@ -349,10 +349,9 @@ impl Attestation { } } - /// Full local verification: runs DCAP (`dcap_qvl::verify::verify`) and then - /// the post-DCAP checks. Behind the `local-verify` feature, which pulls in - /// `dcap-qvl`. Used by off-chain callers and, today, by `mpc-contract`. - // TODO(#3264): contract drops this once DCAP moves to the verifier contract. + /// Full local verification: runs the DCAP quote verification and then the + /// post-DCAP checks. Behind the `local-verify` feature, which pulls in + /// `dcap-qvl`. Used by off-chain callers (node, tee-authority, attestation-cli). #[cfg(feature = "local-verify")] pub fn verify_locally( &self, diff --git a/crates/near-mpc-contract-interface/src/deposits.rs b/crates/near-mpc-contract-interface/src/deposits.rs new file mode 100644 index 0000000000..51a1e91587 --- /dev/null +++ b/crates/near-mpc-contract-interface/src/deposits.rs @@ -0,0 +1,6 @@ +//! Deposit amounts to attach to contract methods, in milli-NEAR. One shared +//! value for node, tests, and e2e. + +/// Deposit for `submit_participant_info`. The contract requires exactly this +/// flat fee to store the bounded attestation entry; nothing is refunded. +pub const SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR: u128 = 100; diff --git a/crates/near-mpc-contract-interface/src/lib.rs b/crates/near-mpc-contract-interface/src/lib.rs index 3965407b61..569fa2cb94 100644 --- a/crates/near-mpc-contract-interface/src/lib.rs +++ b/crates/near-mpc-contract-interface/src/lib.rs @@ -2,6 +2,7 @@ #[cfg(feature = "call-args")] pub mod call_args; +pub mod deposits; pub mod method_names; pub mod types { pub use attestation::{ diff --git a/crates/near-mpc-contract-interface/src/method_names.rs b/crates/near-mpc-contract-interface/src/method_names.rs index 85161e9877..7885f3d9b1 100644 --- a/crates/near-mpc-contract-interface/src/method_names.rs +++ b/crates/near-mpc-contract-interface/src/method_names.rs @@ -65,6 +65,11 @@ pub const RETURN_SIGNATURE_AND_CLEAN_STATE_ON_SUCCESS: &str = pub const RETURN_CK_AND_CLEAN_STATE_ON_SUCCESS: &str = "return_ck_and_clean_state_on_success"; pub const RETURN_VERIFY_FOREIGN_TX_AND_CLEAN_STATE_ON_SUCCESS: &str = "return_verify_foreign_tx_and_clean_state_on_success"; +pub const RESOLVE_VERIFICATION: &str = "resolve_verification"; +pub const FAIL_ATTESTATION_SUBMISSION: &str = "fail_attestation_submission"; + +// TEE verifier contract (the method `mpc-contract` calls cross-contract) +pub const VERIFY_QUOTE: &str = "verify_quote"; // View methods pub const STATE: &str = "state"; diff --git a/crates/near-mpc-contract-interface/src/types/config.rs b/crates/near-mpc-contract-interface/src/types/config.rs index 75646b5a5a..d1f2a3a1e1 100644 --- a/crates/near-mpc-contract-interface/src/types/config.rs +++ b/crates/near-mpc-contract-interface/src/types/config.rs @@ -37,6 +37,8 @@ pub struct InitConfig { pub return_ck_and_clean_state_on_success_call_tera_gas: Option, /// Prepaid gas for a `fail_on_timeout` call. pub fail_on_timeout_tera_gas: Option, + /// Prepaid gas for a `fail_attestation_submission` call. + pub fail_attestation_submission_tera_gas: Option, /// Prepaid gas for a `clean_tee_status` call. pub clean_tee_status_tera_gas: Option, /// Prepaid gas for the reshare-time `clean_invalid_attestations` promise. @@ -49,6 +51,10 @@ pub struct InitConfig { pub clean_foreign_chain_data_tera_gas: Option, /// Prepaid gas for a `remove_non_participant_tee_verifier_votes` call. pub remove_non_participant_tee_verifier_votes_tera_gas: Option, + /// Gas attached to the cross-contract `verify_quote` call on the verifier. + pub verifier_tera_gas: Option, + /// Prepaid gas for the `resolve_verification` callback. + pub resolve_verification_tera_gas: Option, } /// Configuration parameters of the contract. @@ -87,6 +93,8 @@ pub struct Config { pub return_ck_and_clean_state_on_success_call_tera_gas: u64, /// Prepaid gas for a `fail_on_timeout` call. pub fail_on_timeout_tera_gas: u64, + /// Prepaid gas for a `fail_attestation_submission` call. + pub fail_attestation_submission_tera_gas: u64, /// Prepaid gas for a `clean_tee_status` call. pub clean_tee_status_tera_gas: u64, /// Prepaid gas for the reshare-time `clean_invalid_attestations` promise. @@ -99,6 +107,10 @@ pub struct Config { pub clean_foreign_chain_data_tera_gas: u64, /// Prepaid gas for a `remove_non_participant_tee_verifier_votes` call. pub remove_non_participant_tee_verifier_votes_tera_gas: u64, + /// Gas attached to the cross-contract `verify_quote` call on the verifier. + pub verifier_tera_gas: u64, + /// Prepaid gas for the `resolve_verification` callback. + pub resolve_verification_tera_gas: u64, } #[cfg(test)] @@ -117,12 +129,15 @@ mod tests { return_signature_and_clean_state_on_success_call_tera_gas: Some(7), return_ck_and_clean_state_on_success_call_tera_gas: Some(7), fail_on_timeout_tera_gas: Some(2), + fail_attestation_submission_tera_gas: Some(2), clean_tee_status_tera_gas: Some(10), clean_invalid_attestations_tera_gas: Some(10), cleanup_orphaned_node_migrations_tera_gas: Some(3), remove_non_participant_update_votes_tera_gas: Some(5), clean_foreign_chain_data_tera_gas: Some(5), remove_non_participant_tee_verifier_votes_tera_gas: Some(5), + verifier_tera_gas: Some(100), + resolve_verification_tera_gas: Some(60), }; let json = serde_json::to_string(&original_config).unwrap(); let serialized_and_deserialized_config: InitConfig = serde_json::from_str(&json).unwrap(); @@ -167,12 +182,15 @@ mod tests { return_signature_and_clean_state_on_success_call_tera_gas: None, return_ck_and_clean_state_on_success_call_tera_gas: None, fail_on_timeout_tera_gas: None, + fail_attestation_submission_tera_gas: None, clean_tee_status_tera_gas: None, clean_invalid_attestations_tera_gas: None, cleanup_orphaned_node_migrations_tera_gas: None, remove_non_participant_update_votes_tera_gas: None, clean_foreign_chain_data_tera_gas: None, remove_non_participant_tee_verifier_votes_tera_gas: None, + verifier_tera_gas: None, + resolve_verification_tera_gas: None, }; assert_eq!(default_config, config_with_all_values_as_none); diff --git a/crates/node/src/indexer/tx_sender.rs b/crates/node/src/indexer/tx_sender.rs index 7390b76ba0..da378427cc 100644 --- a/crates/node/src/indexer/tx_sender.rs +++ b/crates/node/src/indexer/tx_sender.rs @@ -10,7 +10,7 @@ use crate::types::{ use anyhow::Context; use ed25519_dalek::SigningKey; use near_account_id::AccountId; -use near_indexer_primitives::types::Gas; +use near_indexer_primitives::types::{Balance, Gas}; use near_mpc_contract_interface::types::{Attestation, Ed25519PublicKey, VerifiedAttestation}; use near_time::Clock; use std::future::Future; @@ -148,6 +148,7 @@ async fn submit_tx( method: String, params_ser: String, gas: Gas, + deposit: Balance, ) -> anyhow::Result { let block = indexer_state.view_client.latest_final_block().await?; @@ -156,6 +157,7 @@ async fn submit_tx( method, params_ser.into(), gas, + deposit, block.header.hash, block.header.height, ); @@ -341,6 +343,7 @@ async fn ensure_send_transaction( method.to_string(), params_ser.clone(), request.gas_required(), + request.deposit_required(), ) .await; diff --git a/crates/node/src/indexer/tx_signer.rs b/crates/node/src/indexer/tx_signer.rs index 5d6de6b842..1152fc6d72 100644 --- a/crates/node/src/indexer/tx_signer.rs +++ b/crates/node/src/indexer/tx_signer.rs @@ -36,12 +36,14 @@ impl TransactionSigner { new_nonce } + #[expect(clippy::too_many_arguments)] pub(crate) fn create_and_sign_function_call_tx( &self, receiver_id: AccountId, method_name: String, args: Vec, gas: Gas, + deposit: Balance, block_hash: CryptoHash, block_height: u64, ) -> SignedTransaction { @@ -49,7 +51,7 @@ impl TransactionSigner { method_name, args, gas, - deposit: Balance::from_near(0), + deposit, }; let verifying_key = self.signing_key.verifying_key(); diff --git a/crates/node/src/indexer/types.rs b/crates/node/src/indexer/types.rs index 663cf0e586..b47402041e 100644 --- a/crates/node/src/indexer/types.rs +++ b/crates/node/src/indexer/types.rs @@ -6,8 +6,9 @@ use k256::{ ecdsa::RecoveryId, elliptic_curve::{Curve, CurveArithmetic, ops::Reduce, point::AffineCoordinates}, }; -use near_indexer_primitives::types::Gas; +use near_indexer_primitives::types::{Balance, Gas}; use near_mpc_contract_interface::call_args as contract_args; +use near_mpc_contract_interface::deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR; use near_mpc_contract_interface::method_names::{ CONCLUDE_NODE_MIGRATION, REGISTER_FOREIGN_CHAINS_CONFIG, RESPOND, RESPOND_CKD, RESPOND_VERIFY_FOREIGN_TX, START_KEYGEN_INSTANCE, START_RESHARE_INSTANCE, @@ -126,6 +127,15 @@ impl ChainSendTransactionRequest { | Self::VerifyForeignTransactionRespond(_) => MAX_GAS, } } + + pub fn deposit_required(&self) -> Balance { + match self { + Self::SubmitParticipantInfo { .. } => { + Balance::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR) + } + _ => Balance::from_near(0), + } + } } /// Extension trait for constructing SignatureRespond arguments from node-internal types. diff --git a/crates/tee-context/src/lib.rs b/crates/tee-context/src/lib.rs index 0088af5cda..f941478c18 100644 --- a/crates/tee-context/src/lib.rs +++ b/crates/tee-context/src/lib.rs @@ -11,13 +11,14 @@ use chain_gateway::{ types::FunctionCallArgs, }; use near_account_id::AccountId; -use near_mpc_contract_interface::call_args as contract_args; -use near_mpc_contract_interface::method_names::{ - ALLOWED_DOCKER_IMAGE_HASHES, ALLOWED_LAUNCHER_COMPOSE_HASHES, SUBMIT_PARTICIPANT_INFO, - VERIFY_TEE, -}; -use near_mpc_contract_interface::types::{ - AllowedMpcDockerImageHash, Attestation, Ed25519PublicKey, +use near_mpc_contract_interface::{ + call_args as contract_args, + deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, + method_names::{ + ALLOWED_DOCKER_IMAGE_HASHES, ALLOWED_LAUNCHER_COMPOSE_HASHES, SUBMIT_PARTICIPANT_INFO, + VERIFY_TEE, + }, + types::{AllowedMpcDockerImageHash, Attestation, Ed25519PublicKey}, }; use serde::Deserialize; use tokio::sync::watch; @@ -133,7 +134,7 @@ where method_name: SUBMIT_PARTICIPANT_INFO.to_string(), args: args_json, gas: SUBMIT_ATTESTATION_GAS, - deposit: NearToken::from_yoctonear(0), + deposit: NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR), }, ) .await @@ -151,7 +152,7 @@ where method_name: VERIFY_TEE.to_string(), args: b"{}".to_vec(), gas: VERIFY_TEE_GAS, - deposit: NearToken::from_yoctonear(0), + deposit: NearToken::from_near(0), }, ) .await diff --git a/crates/test-utils/src/contract_types.rs b/crates/test-utils/src/contract_types.rs index 6334ab1536..95809b87d6 100644 --- a/crates/test-utils/src/contract_types.rs +++ b/crates/test-utils/src/contract_types.rs @@ -15,5 +15,8 @@ pub fn dummy_config(value: u64) -> near_mpc_contract_interface::types::Config { remove_non_participant_update_votes_tera_gas: value + 11, clean_foreign_chain_data_tera_gas: value + 12, remove_non_participant_tee_verifier_votes_tera_gas: value + 13, + verifier_tera_gas: value + 14, + resolve_verification_tera_gas: value + 15, + fail_attestation_submission_tera_gas: value + 16, } }