Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a0e4c88
refactor(attestation): split DCAP verification behind a local-verify …
pbeza Jun 16, 2026
2bc5d99
docs(attestation): fix verify_locally docs to match PR behavior; rena…
pbeza Jun 16, 2026
a0c21c0
refactor(mpc-attestation): debug_assert on verify_mock_only type misuse
pbeza Jun 16, 2026
af1aaaf
Merge remote-tracking branch 'origin/main' into refactor/attestation-…
pbeza Jun 17, 2026
ad371d3
refactor: share dcap_qvl conversions and per-variant attestation verify
pbeza Jun 17, 2026
231e48d
Merge remote-tracking branch 'origin/main' into refactor/attestation-…
pbeza Jun 17, 2026
07e7b22
refactor(attestation): polish per-variant verify API and docs
pbeza Jun 17, 2026
0fddb4a
Merge remote-tracking branch 'origin/main' into refactor/attestation-…
pbeza Jun 17, 2026
8a4761c
refactor(mpc-attestation): extract MockAttestation::verify_constraints
pbeza Jun 18, 2026
fc1129a
refactor(mpc-attestation): route verify_locally's Dstack arm through …
pbeza Jun 18, 2026
118231d
Merge remote-tracking branch 'origin/main' into refactor/attestation-…
pbeza Jun 18, 2026
63fb5be
Merge remote-tracking branch 'origin/main' into refactor/attestation-…
pbeza Jun 18, 2026
036c603
feat(contract): TEE verifier-account voting + lower attestation expiry
pbeza Jun 18, 2026
f19436e
Address PR review: no-op vote guard, intra-doc links, tests, doc nits
pbeza Jun 18, 2026
8410789
feat(contract): add TeeVerifierCodeHash newtype for verifier voting
pbeza Jun 19, 2026
c1d0fe9
Merge remote-tracking branch 'origin/main' into refactor/tee-verifier…
pbeza Jun 19, 2026
46d04a0
Merge remote-tracking branch 'origin/main' into refactor/tee-verifier…
pbeza Jun 22, 2026
79ac827
refactor(contract): make tee_verifier_account_id optional, dedup thre…
pbeza Jun 22, 2026
a07d1a7
test(contract): regenerate ABI snapshot for shortened InitConfig doc
pbeza Jun 22, 2026
2bac9f8
refactor(contract): drop tee_verifier_account_id from InitConfig
pbeza Jun 22, 2026
76fa0c7
refactor(contract): give verifier-vote sweep its own post-resharing c…
pbeza Jun 22, 2026
802e9b4
fix(contract): migrate old Config layout and bump reshare gas
pbeza Jun 22, 2026
5fc2819
refactor(contract): drop V1 suffix from TEE verifier vote storage keys
pbeza Jun 23, 2026
c48bea6
docs(attestation): clarify expiry applies to all accepted attestations
pbeza Jun 23, 2026
f0f4824
Merge remote-tracking branch 'origin/main' into refactor/tee-verifier…
pbeza Jun 23, 2026
337ab10
feat(contract): expose tee_verifier_votes view and test post-resharin…
pbeza Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/attestation-cli/tests/test_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use attestation_cli::cli::Cli;
use attestation_cli::verify;
use mpc_attestation::attestation::Attestation;
use mpc_attestation::attestation::{Attestation, DEFAULT_EXPIRATION_DURATION_SECONDS};
use near_mpc_crypto_types::Ed25519PublicKey;
use node_types::http_server::StaticWebData;
use test_utils::attestation::{
Expand Down Expand Up @@ -56,7 +56,7 @@ fn full_verification_succeeds_with_valid_attestation() {
assert_eq!(result.mpc_image_hash.as_hex(), TEST_MPC_IMAGE_DIGEST_HEX);
assert_eq!(
result.expiry_timestamp_seconds,
VALID_ATTESTATION_TIMESTAMP + 60 * 60 * 24 * 7
VALID_ATTESTATION_TIMESTAMP + DEFAULT_EXPIRATION_DURATION_SECONDS
);
}

Expand Down
6 changes: 6 additions & 0 deletions crates/contract/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const DEFAULT_CLEANUP_ORPHANED_NODE_MIGRATIONS_TERA_GAS: u64 = 4;
const DEFAULT_REMOVE_NON_PARTICIPANT_UPDATE_VOTES_TERA_GAS: u64 = 5;
/// Prepaid gas for a `clean_foreign_chain_data` call
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;

/// Config for V2 of the contract.
#[near(serializers=[borsh, json])]
Expand Down Expand Up @@ -64,6 +66,8 @@ pub(crate) struct Config {
pub(crate) remove_non_participant_update_votes_tera_gas: u64,
/// Prepaid gas for a `clean_foreign_chain_data` call.
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,
}

impl Default for Config {
Expand All @@ -88,6 +92,8 @@ impl Default for Config {
remove_non_participant_update_votes_tera_gas:
DEFAULT_REMOVE_NON_PARTICIPANT_UPDATE_VOTES_TERA_GAS,
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,
}
}
}
7 changes: 7 additions & 0 deletions crates/contract/src/dto_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ impl From<near_mpc_contract_interface::types::InitConfig> for Config {
if let Some(v) = config_ext.clean_foreign_chain_data_tera_gas {
config.clean_foreign_chain_data_tera_gas = v;
}
if let Some(v) = config_ext.remove_non_participant_tee_verifier_votes_tera_gas {
config.remove_non_participant_tee_verifier_votes_tera_gas = v;
}

config
}
Expand Down Expand Up @@ -514,6 +517,8 @@ impl From<&Config> for near_mpc_contract_interface::types::Config {
remove_non_participant_update_votes_tera_gas: value
.remove_non_participant_update_votes_tera_gas,
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,
}
}
}
Expand All @@ -540,6 +545,8 @@ impl From<near_mpc_contract_interface::types::Config> for Config {
remove_non_participant_update_votes_tera_gas: value
.remove_non_participant_update_votes_tera_gas,
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,
}
}
}
Expand Down
179 changes: 147 additions & 32 deletions crates/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ use crate::{
ckd::{CKDRequest, app_public_key_check, ckd_output_check},
domain::AddDomainsVotes,
},
state::ContractNotInitialized,
storage_keys::StorageKey,
tee::tee_state::{TeeQuoteStatus, TeeState},
tee::verifier_votes::{TeeVerifierVotes, VerifierChangeProposal},
update::{ProposeUpdateArgs, ProposedUpdates, Update, UpdateId},
};
use config::Config;
Expand All @@ -70,7 +70,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_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash};
use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash, TeeVerifierCodeHash};
use near_sdk::{
AccountId, CryptoHash, Gas, GasWeight, NearToken, Promise, PromiseError, PromiseOrValue, env,
log, near,
Expand Down Expand Up @@ -163,6 +163,12 @@ pub struct MpcContract {
// TODO(#2937): Remove via state migration.
metrics: Metrics,
foreign_chains: Lazy<ForeignChainsMetadata>,
/// The verifier contract account trusted for DCAP verification, or [`None`]
/// until participants vote one in. Not yet used to dispatch verification.
// 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<AccountId>,
tee_verifier_votes: TeeVerifierVotes,
}

#[near(serializers=[borsh])]
Expand Down Expand Up @@ -1214,6 +1220,18 @@ impl MpcContract {
Gas::from_tgas(self.config.clean_foreign_chain_data_tera_gas),
)
.detach();
// Spawn a promise to drop verifier-change votes cast by non-participants
Promise::new(env::current_account_id())
.function_call(
method_names::REMOVE_NON_PARTICIPANT_TEE_VERIFIER_VOTES.to_string(),
vec![],
NearToken::from_yoctonear(0),
Gas::from_tgas(
self.config
.remove_non_participant_tee_verifier_votes_tera_gas,
),
)
.detach();
}

Ok(())
Expand Down Expand Up @@ -1393,12 +1411,7 @@ impl MpcContract {
);
self.voter_or_panic();

let threshold_parameters = match self.protocol_state.threshold_parameters() {
Ok(threshold_parameters) => threshold_parameters,
Err(ContractNotInitialized) => env::panic_str(
"Contract is not initialized. Can not vote for a new image hash before initialization.",
),
};
let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();
Comment thread
pbeza marked this conversation as resolved.

let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;
let votes = self.tee_state.vote(code_hash, &participant);
Expand Down Expand Up @@ -1431,12 +1444,7 @@ impl MpcContract {
);
self.voter_or_panic();

let threshold_parameters = match self.protocol_state.threshold_parameters() {
Ok(threshold_parameters) => threshold_parameters,
Err(ContractNotInitialized) => env::panic_str(
"Contract is not initialized. Cannot vote for a new launcher hash before initialization.",
),
};
let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();

let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;
let action = LauncherVoteAction::Add(launcher_hash);
Expand Down Expand Up @@ -1469,12 +1477,7 @@ impl MpcContract {
);
self.voter_or_panic();

let threshold_parameters = match self.protocol_state.threshold_parameters() {
Ok(threshold_parameters) => threshold_parameters,
Err(ContractNotInitialized) => env::panic_str(
"Contract is not initialized. Cannot vote to remove a launcher hash before initialization.",
),
};
let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();

let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;
let action = LauncherVoteAction::Remove(launcher_hash);
Expand Down Expand Up @@ -1503,12 +1506,7 @@ impl MpcContract {
);
self.voter_or_panic();

let threshold_parameters = match self.protocol_state.threshold_parameters() {
Ok(threshold_parameters) => threshold_parameters,
Err(ContractNotInitialized) => env::panic_str(
"Contract is not initialized. Cannot vote for an OS measurement before initialization.",
),
};
let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();

let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;
let action = MeasurementVoteAction::Add(measurement.clone());
Expand Down Expand Up @@ -1536,12 +1534,7 @@ impl MpcContract {
);
self.voter_or_panic();

let threshold_parameters = match self.protocol_state.threshold_parameters() {
Ok(threshold_parameters) => threshold_parameters,
Err(ContractNotInitialized) => env::panic_str(
"Contract is not initialized. Cannot vote to remove an OS measurement before initialization.",
),
};
let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();

let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;
let action = MeasurementVoteAction::Remove(measurement.clone());
Expand Down Expand Up @@ -1615,6 +1608,63 @@ impl MpcContract {
Ok(applied)
}

/// Vote for a candidate account to become the trusted verifier contract
/// account, committing to the code hash the voter audited. When the proposal
/// crosses the signing threshold, the trusted verifier account is updated
/// and all pending verifier-change votes are cleared.
#[handle_result]
pub fn vote_tee_verifier_change(
&mut self,
candidate_account_id: AccountId,
expected_code_hash: TeeVerifierCodeHash,
) -> Result<(), Error> {
log!(
"vote_tee_verifier_change: signer={}, candidate={}, expected_code_hash={}",
env::signer_account_id(),
candidate_account_id,
expected_code_hash,
);
self.voter_or_panic();

// Voting in the already-current verifier is a no-op
if self.tee_verifier_account_id.as_ref() == Some(&candidate_account_id) {
return Ok(());
}

let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();
let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;

let proposal = VerifierChangeProposal {
candidate_account_id,
expected_code_hash,
};
if let Some(new_verifier) =
self.tee_verifier_votes
.vote(proposal, participant, threshold_parameters)?
{
log!("vote_tee_verifier_change: new verifier = {}", new_verifier);
self.tee_verifier_account_id = Some(new_verifier);
}
Ok(())
}

/// Withdraw the caller's current vote on any pending verifier-change
/// proposal. No-op if the caller has not voted.
#[handle_result]
pub fn withdraw_tee_verifier_vote(&mut self) -> Result<(), Error> {
log!(
"withdraw_tee_verifier_vote: signer={}",
env::signer_account_id(),
);
self.voter_or_panic();

let threshold_parameters = self.protocol_state.threshold_parameters_or_panic();
let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?;

self.tee_verifier_votes.withdraw(&participant);
Ok(())
}

/// On-chain RPC provider whitelist keyed by `ForeignChain`. Nodes read this at
/// startup to validate their local `foreign_chains.yaml`. Borsh-encoded result.
#[result_serializer(borsh)]
Expand Down Expand Up @@ -1843,6 +1893,28 @@ impl MpcContract {

Ok(())
}

/// Private endpoint to drop verifier-change votes cast by non-participants
/// after resharing.
#[private]
#[handle_result]
pub fn remove_non_participant_tee_verifier_votes(&mut self) -> Result<(), Error> {
log!(
"remove_non_participant_tee_verifier_votes: signer={}",
env::signer_account_id()
);

let participants = match &self.protocol_state {
ProtocolContractState::Running(state) => state.parameters.participants(),
_ => {
return Err(InvalidState::ProtocolStateNotRunning.into());
}
};

self.tee_verifier_votes.retain(participants);

Ok(())
}
}

// Contract developer helper API
Expand Down Expand Up @@ -1894,6 +1966,8 @@ impl MpcContract {
StorageKey::ForeignChainMetadata,
ForeignChainsMetadata::default(),
),
tee_verifier_account_id: None,
tee_verifier_votes: TeeVerifierVotes::default(),
Comment thread
pbeza marked this conversation as resolved.
})
}

Expand Down Expand Up @@ -1971,6 +2045,8 @@ impl MpcContract {
StorageKey::ForeignChainMetadata,
ForeignChainsMetadata::default(),
),
tee_verifier_account_id: None,
tee_verifier_votes: TeeVerifierVotes::default(),
})
}

Expand Down Expand Up @@ -3790,6 +3866,43 @@ mod tests {
(contract, participants, first_participant_id)
}

#[test]
#[expect(non_snake_case)]
fn vote_tee_verifier_change__should_apply_candidate_when_threshold_reached() {
// Given a running contract with 3 participants, signing threshold 2,
// starting unconfigured.
let (mut contract, participants, _) = setup_tee_test_contract(3, 2);
assert_eq!(contract.tee_verifier_account_id, None);
let participant_account_ids: Vec<AccountId> = participants
.participants()
.iter()
.map(|(account_id, _, _)| account_id.clone())
.collect();
let candidate: AccountId = "verifier.near".parse().unwrap();
let code_hash = TeeVerifierCodeHash::new([7u8; 32]);

let vote_as = |contract: &mut MpcContract, account_id: &AccountId| {
testing_env!(
VMContextBuilder::new()
.signer_account_id(account_id.clone())
.predecessor_account_id(account_id.clone())
.build()
);
contract
.vote_tee_verifier_change(candidate.clone(), code_hash)
.expect("vote should succeed");
};

// When the first participant votes (below threshold), the verifier is unchanged.
vote_as(&mut contract, &participant_account_ids[0]);
assert_eq!(contract.tee_verifier_account_id, None);

// When the second participant votes, threshold is reached and the
// candidate becomes the trusted verifier.
vote_as(&mut contract, &participant_account_ids[1]);
assert_eq!(contract.tee_verifier_account_id, Some(candidate));
}

fn submit_attestation(
contract: &mut MpcContract,
participants: &Participants,
Expand Down Expand Up @@ -4399,6 +4512,8 @@ mod tests {
StorageKey::ForeignChainMetadata,
ForeignChainsMetadata::default(),
),
tee_verifier_account_id: None,
tee_verifier_votes: Default::default(),
}
}
}
Expand Down
Loading
Loading