|
| 1 | +//! The account trusted to verify DCAP quotes, and the participant votes that |
| 2 | +//! change it. |
| 3 | +
|
| 4 | +use crate::dto_mapping::IntoInterfaceType; |
| 5 | +use crate::errors::{Error, InvalidState}; |
| 6 | +use crate::primitives::key_state::AuthenticatedParticipantId; |
| 7 | +use crate::primitives::votes::ProposalHash; |
| 8 | +use crate::state::ProtocolContractState; |
| 9 | +use crate::tee::verifier_votes::VerifierChangeProposal; |
| 10 | +use crate::{MpcContract, MpcContractExt}; |
| 11 | +use mpc_primitives::hash::TeeVerifierCodeHash; |
| 12 | +use near_mpc_contract_interface::types as dtos; |
| 13 | +use near_sdk::{AccountId, env, log, near}; |
| 14 | +use std::collections::{BTreeMap, BTreeSet}; |
| 15 | + |
| 16 | +#[near] |
| 17 | +impl MpcContract { |
| 18 | + /// Vote for a candidate account to become the trusted verifier contract |
| 19 | + /// account, committing to the code hash the voter audited. When the proposal |
| 20 | + /// crosses the signing threshold, the trusted verifier account is updated |
| 21 | + /// and all pending verifier-change votes are cleared. |
| 22 | + #[handle_result] |
| 23 | + pub fn vote_tee_verifier_change( |
| 24 | + &mut self, |
| 25 | + candidate_account_id: AccountId, |
| 26 | + expected_code_hash: TeeVerifierCodeHash, |
| 27 | + ) -> Result<(), Error> { |
| 28 | + log!( |
| 29 | + "vote_tee_verifier_change: signer={}, candidate={}, expected_code_hash={}", |
| 30 | + env::signer_account_id(), |
| 31 | + candidate_account_id, |
| 32 | + expected_code_hash, |
| 33 | + ); |
| 34 | + self.voter_or_panic(); |
| 35 | + |
| 36 | + // Voting in the already-current verifier is a no-op |
| 37 | + if self.tee_verifier_account_id.as_ref() == Some(&candidate_account_id) { |
| 38 | + return Ok(()); |
| 39 | + } |
| 40 | + |
| 41 | + let threshold_parameters = self.protocol_state.threshold_parameters_or_panic(); |
| 42 | + let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?; |
| 43 | + |
| 44 | + let proposal = VerifierChangeProposal { |
| 45 | + candidate_account_id, |
| 46 | + expected_code_hash, |
| 47 | + }; |
| 48 | + if let Some(new_verifier) = |
| 49 | + self.tee_verifier_votes |
| 50 | + .vote(proposal, participant, threshold_parameters)? |
| 51 | + { |
| 52 | + log!("vote_tee_verifier_change: new verifier = {}", new_verifier); |
| 53 | + self.tee_verifier_account_id = Some(new_verifier); |
| 54 | + } |
| 55 | + Ok(()) |
| 56 | + } |
| 57 | + |
| 58 | + /// Withdraw the caller's current vote on any pending verifier-change |
| 59 | + /// proposal. No-op if the caller has not voted. |
| 60 | + #[handle_result] |
| 61 | + pub fn withdraw_tee_verifier_vote(&mut self) -> Result<(), Error> { |
| 62 | + log!( |
| 63 | + "withdraw_tee_verifier_vote: signer={}", |
| 64 | + env::signer_account_id(), |
| 65 | + ); |
| 66 | + self.voter_or_panic(); |
| 67 | + |
| 68 | + let threshold_parameters = self.protocol_state.threshold_parameters_or_panic(); |
| 69 | + let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?; |
| 70 | + |
| 71 | + self.tee_verifier_votes.withdraw(&participant); |
| 72 | + Ok(()) |
| 73 | + } |
| 74 | + |
| 75 | + /// Private endpoint to drop verifier-change votes cast by non-participants |
| 76 | + /// after resharing. |
| 77 | + #[private] |
| 78 | + #[handle_result] |
| 79 | + pub fn remove_non_participant_tee_verifier_votes(&mut self) -> Result<(), Error> { |
| 80 | + log!( |
| 81 | + "remove_non_participant_tee_verifier_votes: signer={}", |
| 82 | + env::signer_account_id() |
| 83 | + ); |
| 84 | + |
| 85 | + let participants = match &self.protocol_state { |
| 86 | + ProtocolContractState::Running(state) => state.parameters.participants(), |
| 87 | + _ => { |
| 88 | + return Err(InvalidState::ProtocolStateNotRunning.into()); |
| 89 | + } |
| 90 | + }; |
| 91 | + |
| 92 | + self.tee_verifier_votes.retain(participants); |
| 93 | + |
| 94 | + Ok(()) |
| 95 | + } |
| 96 | + |
| 97 | + /// Returns the pending TEE verifier-change votes, keyed by proposal. |
| 98 | + pub fn tee_verifier_votes( |
| 99 | + &self, |
| 100 | + ) -> BTreeMap<ProposalHash, BTreeSet<dtos::AuthenticatedParticipantId>> { |
| 101 | + self.tee_verifier_votes |
| 102 | + .pending() |
| 103 | + .iter() |
| 104 | + .map(|(proposal, voters)| { |
| 105 | + ( |
| 106 | + *proposal, |
| 107 | + voters.iter().map(|v| v.into_dto_type()).collect(), |
| 108 | + ) |
| 109 | + }) |
| 110 | + .collect() |
| 111 | + } |
| 112 | + |
| 113 | + /// Returns the trusted TEE verifier contract account, or [`None`] until |
| 114 | + /// participants vote one in via [`Self::vote_tee_verifier_change`]. |
| 115 | + pub fn tee_verifier_account_id(&self) -> Option<AccountId> { |
| 116 | + self.tee_verifier_account_id.clone() |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +#[cfg(not(target_arch = "wasm32"))] |
| 121 | +#[cfg(test)] |
| 122 | +#[expect(non_snake_case)] |
| 123 | +mod tests { |
| 124 | + use super::*; |
| 125 | + use crate::api::test_utils::{participant_account_ids, setup_tee_test_contract}; |
| 126 | + use crate::primitives::thresholds::{GovernanceThreshold, GovernanceThresholdParameters}; |
| 127 | + use crate::state::key_event::tests::Environment; |
| 128 | + use near_sdk::test_utils::VMContextBuilder; |
| 129 | + use near_sdk::testing_env; |
| 130 | + use std::panic; |
| 131 | + |
| 132 | + #[test] |
| 133 | + fn vote_tee_verifier_change__should_apply_candidate_when_threshold_reached() { |
| 134 | + // Given a running contract with 3 participants, signing threshold 2, |
| 135 | + // starting unconfigured. |
| 136 | + let (mut contract, participants, _) = setup_tee_test_contract(3, 2); |
| 137 | + assert_eq!(contract.tee_verifier_account_id, None); |
| 138 | + let participant_account_ids: Vec<AccountId> = participants |
| 139 | + .participants() |
| 140 | + .iter() |
| 141 | + .map(|(account_id, _, _)| account_id.clone()) |
| 142 | + .collect(); |
| 143 | + let candidate: AccountId = "verifier.near".parse().unwrap(); |
| 144 | + let code_hash = TeeVerifierCodeHash::new([7u8; 32]); |
| 145 | + |
| 146 | + let vote_as = |contract: &mut MpcContract, account_id: &AccountId| { |
| 147 | + testing_env!( |
| 148 | + VMContextBuilder::new() |
| 149 | + .signer_account_id(account_id.clone()) |
| 150 | + .predecessor_account_id(account_id.clone()) |
| 151 | + .build() |
| 152 | + ); |
| 153 | + contract |
| 154 | + .vote_tee_verifier_change(candidate.clone(), code_hash) |
| 155 | + .expect("vote should succeed"); |
| 156 | + }; |
| 157 | + |
| 158 | + // When the first participant votes (below threshold), the verifier is unchanged. |
| 159 | + vote_as(&mut contract, &participant_account_ids[0]); |
| 160 | + assert_eq!(contract.tee_verifier_account_id, None); |
| 161 | + |
| 162 | + // When the second participant votes, threshold is reached and the |
| 163 | + // candidate becomes the trusted verifier. |
| 164 | + vote_as(&mut contract, &participant_account_ids[1]); |
| 165 | + assert_eq!(contract.tee_verifier_account_id, Some(candidate)); |
| 166 | + } |
| 167 | + |
| 168 | + #[test] |
| 169 | + fn tee_verifier_account_id__should_report_none_until_threshold_then_the_candidate() { |
| 170 | + // Given |
| 171 | + let (mut contract, _, _) = setup_tee_test_contract(3, 2); |
| 172 | + let voters = participant_account_ids(&contract); |
| 173 | + let candidate: AccountId = "verifier.near".parse().unwrap(); |
| 174 | + let code_hash = TeeVerifierCodeHash::new([7u8; 32]); |
| 175 | + |
| 176 | + let vote_as = |contract: &mut MpcContract, account_id: &AccountId| { |
| 177 | + Environment::new(None, Some(account_id.clone()), None); |
| 178 | + contract |
| 179 | + .vote_tee_verifier_change(candidate.clone(), code_hash) |
| 180 | + .expect("vote should succeed"); |
| 181 | + }; |
| 182 | + |
| 183 | + assert_eq!(contract.tee_verifier_account_id(), None); |
| 184 | + |
| 185 | + // When |
| 186 | + vote_as(&mut contract, &voters[0]); |
| 187 | + assert_eq!(contract.tee_verifier_account_id(), None); |
| 188 | + vote_as(&mut contract, &voters[1]); |
| 189 | + |
| 190 | + // Then |
| 191 | + assert_eq!(contract.tee_verifier_account_id(), Some(candidate)); |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn remove_non_participant_tee_verifier_votes__should_drop_votes_from_dropped_participants() { |
| 196 | + // Given a running contract with 3 participants, signing threshold 3, where |
| 197 | + // two participants have cast votes for distinct candidates (neither crosses |
| 198 | + // threshold, so both stay pending). |
| 199 | + let (mut contract, participants, _) = setup_tee_test_contract(3, 3); |
| 200 | + let voters = participant_account_ids(&contract); |
| 201 | + let code_hash = TeeVerifierCodeHash::new([7u8; 32]); |
| 202 | + |
| 203 | + // Vote as `account_id` for `candidate`, returning that voter's authenticated id. |
| 204 | + let vote_as = |
| 205 | + |contract: &mut MpcContract, account_id: &AccountId, candidate: &AccountId| { |
| 206 | + Environment::new(None, Some(account_id.clone()), None); |
| 207 | + contract |
| 208 | + .vote_tee_verifier_change(candidate.clone(), code_hash) |
| 209 | + .expect("vote should succeed"); |
| 210 | + AuthenticatedParticipantId::new(&participants).unwrap() |
| 211 | + }; |
| 212 | + |
| 213 | + // The single-voter pending bucket: proposal(candidate) -> {voter}. |
| 214 | + let bucket = |candidate: &AccountId, voter: &AuthenticatedParticipantId| { |
| 215 | + let proposal = VerifierChangeProposal { |
| 216 | + candidate_account_id: candidate.clone(), |
| 217 | + expected_code_hash: code_hash, |
| 218 | + }; |
| 219 | + ( |
| 220 | + ProposalHash::from(proposal), |
| 221 | + BTreeSet::from([voter.into_dto_type()]), |
| 222 | + ) |
| 223 | + }; |
| 224 | + |
| 225 | + let candidate_a: AccountId = "verifier-a.near".parse().unwrap(); |
| 226 | + let candidate_b: AccountId = "verifier-b.near".parse().unwrap(); |
| 227 | + let auth_a = vote_as(&mut contract, &voters[0], &candidate_a); |
| 228 | + let auth_b = vote_as(&mut contract, &voters[1], &candidate_b); |
| 229 | + |
| 230 | + // Then both single-voter buckets are pending. |
| 231 | + assert_eq!( |
| 232 | + contract.tee_verifier_votes(), |
| 233 | + BTreeMap::from([bucket(&candidate_a, &auth_a), bucket(&candidate_b, &auth_b)]), |
| 234 | + ); |
| 235 | + |
| 236 | + // When resharing drops the first participant and the post-resharing cleanup runs. |
| 237 | + { |
| 238 | + let ProtocolContractState::Running(ref mut state) = contract.protocol_state else { |
| 239 | + panic!("expected Running"); |
| 240 | + }; |
| 241 | + state.parameters = GovernanceThresholdParameters::new( |
| 242 | + participants.subset(1..3), |
| 243 | + GovernanceThreshold::new(2), |
| 244 | + ) |
| 245 | + .unwrap(); |
| 246 | + } |
| 247 | + Environment::new(None, Some(env::current_account_id()), None); |
| 248 | + contract |
| 249 | + .remove_non_participant_tee_verifier_votes() |
| 250 | + .unwrap(); |
| 251 | + |
| 252 | + // Then only the still-participant's vote (candidate B) remains. |
| 253 | + assert_eq!( |
| 254 | + contract.tee_verifier_votes(), |
| 255 | + BTreeMap::from([bucket(&candidate_b, &auth_b)]), |
| 256 | + ); |
| 257 | + } |
| 258 | +} |
0 commit comments