Skip to content

Commit 731b314

Browse files
authored
refactor(contract): move TEE verifier votes into api/tee_verifier.rs (#4193)
1 parent 216243c commit 731b314

3 files changed

Lines changed: 262 additions & 234 deletions

File tree

crates/contract/src/api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod keys;
99
pub mod lifecycle;
1010
pub mod node_migration;
1111
pub mod sign;
12+
pub mod tee_verifier;
1213
#[cfg(not(target_arch = "wasm32"))]
1314
#[cfg(test)]
1415
pub mod test_utils;
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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

Comments
 (0)