Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6a1579a
refactor(contract): move parameter votes from RunningContractState to…
SimonRastikian Oct 20, 2026
0365e39
fix(contract): don't box InitializingContractState in 3.17 migration …
SimonRastikian Oct 20, 2026
6fb287f
Merge branch 'main' into 2825-replace-GovernanceThresholdParametersVo…
SimonRastikian Oct 20, 2026
c8f90c7
test(contract): pass the votes registry to process_new_parameters_pro…
SimonRastikian Oct 20, 2026
52df55f
refactor(contract): address review feedback on the parameter-votes move
SimonRastikian Oct 20, 2026
e1837be
test(contract): split the re-proposal vote and reshare-clear cases
SimonRastikian Oct 20, 2026
cb00e0d
test(contract): use the Environment helper for caller contexts in gov…
SimonRastikian Oct 22, 2026
6bac975
refactor(contract): take &self in ResharingContractState::vote_new_pa…
SimonRastikian Oct 22, 2026
8b74519
docs(contract): track dropping the 3.17.0 parameter-votes shadow
SimonRastikian Oct 23, 2026
244018d
Merge remote-tracking branch 'origin/main' into 2825-replace-Governan…
SimonRastikian Oct 23, 2026
2b7e772
Merge remote-tracking branch 'origin/main' into 2825-replace-Governan…
SimonRastikian Oct 27, 2026
33c7b47
docs(contract): point the 3.17.0 leftover TODOs at #4984
SimonRastikian Oct 28, 2026
0215a2d
Merge remote-tracking branch 'origin/main' into 2825-replace-Governan…
SimonRastikian Oct 29, 2026
347cd98
chore(contract): migrate from the 3.18.0 contract and archive its wasm
SimonRastikian Oct 29, 2026
ed025bd
Revert "chore(contract): migrate from the 3.18.0 contract and archive…
SimonRastikian Oct 29, 2026
1de35aa
Merge remote-tracking branch 'origin/main' into 2825-replace-Governan…
SimonRastikian Oct 29, 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
12 changes: 11 additions & 1 deletion crates/contract/src/api/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ impl MpcContract {
BTreeMap::new(),
);
let res = running_state.transition_to_resharing_no_checks(&proposed_parameters);
self.parameters_votes.clear();
if let Some(resharing) = res {
self.protocol_state = ProtocolContractState::Resharing(resharing);
}
Expand Down Expand Up @@ -488,7 +489,9 @@ mod tests {
submit_valid_attestations,
};
use crate::config::Config;
use crate::primitives::key_state::{AttemptId, EpochId, KeyForDomain, Keyset};
use crate::primitives::key_state::{
AttemptId, AuthenticatedAccountId, EpochId, KeyForDomain, Keyset,
};
use crate::primitives::participants::{ParticipantId, Participants};
use crate::primitives::test_utils::{
bogus_ed25519_near_public_key, bogus_ed25519_public_key, bogus_tee_verifier_account_id,
Expand Down Expand Up @@ -1001,6 +1004,12 @@ mod tests {
.block_timestamp((ATTESTATION_EXPIRY_SECONDS + 1) * 1_000_000_000) // nanoseconds
.build()
);
// A pending parameter vote must not survive the kickout's transition to Resharing.
let voter = AuthenticatedAccountId::new(parameters.participants()).unwrap();
let pending_proposal =
ProposedGovernanceThresholdParameters::new(parameters.clone(), BTreeMap::new());
contract.parameters_votes.vote(&pending_proposal, voter);
assert!(!contract.parameters_votes.is_empty());

// Call verify_tee - should trigger resharing
let result = contract.verify_tee();
Expand All @@ -1009,6 +1018,7 @@ mod tests {
Ok(true),
"verify_tee should return Ok(true) when threshold is met"
);
assert!(contract.parameters_votes.is_empty());

// Verify contract transitioned to Resharing state
let ProtocolContractState::Resharing(resharing_state) = &contract.protocol_state else {
Expand Down
143 changes: 134 additions & 9 deletions crates/contract/src/api/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ impl MpcContract {
let proposed_participants = proposal.participants();
match validation_result {
TeeValidationResult::Full => {
if let Some(new_state) = self
.protocol_state
.vote_new_parameters(prospective_epoch_id, &proposal)?
{
if let Some(new_state) = self.protocol_state.vote_new_parameters(
prospective_epoch_id,
&proposal,
&mut self.parameters_votes,
)? {
self.protocol_state = new_state;
}
Ok(())
Expand Down Expand Up @@ -99,6 +100,7 @@ impl MpcContract {

if let Some(new_state) = self.protocol_state.vote_add_domains(domains)? {
self.protocol_state = new_state;
self.parameters_votes.clear();
}
Ok(())
}
Expand All @@ -121,12 +123,15 @@ mod tests {
};
use crate::dto_mapping::IntoInterfaceType;
use crate::errors::{DomainError, InvalidCandidateSet, InvalidState, InvalidThreshold};
use crate::primitives::key_state::{AttemptId, KeyForDomain, Keyset};
use crate::primitives::key_state::{AttemptId, EpochId, KeyEventId, KeyForDomain, Keyset};
use crate::primitives::participants::Participants;
use crate::primitives::test_utils::{bogus_tee_verifier_account_id, gen_participants};
use crate::primitives::threshold_votes::GovernanceThresholdParametersVotes;
use crate::primitives::test_utils::{
authenticate_account_as, bogus_tee_verifier_account_id, gen_participants,
};
use crate::primitives::thresholds::GovernanceThresholdParameters;
use crate::state::ProtocolContractState;
use crate::state::key_event::tests::find_leader;
use crate::tee::test_utils::Environment;
use dtos::{Curve, DomainId, ParticipantCount, Protocol, ReconstructionThreshold};
use near_mpc_contract_interface::types::DomainPurpose;
use near_sdk::test_utils::VMContextBuilder;
Expand Down Expand Up @@ -482,11 +487,131 @@ mod tests {
Error::InvalidState(InvalidState::NotParticipant { account_id }) if account_id == outsider
);
// Check that vote was not accepted.
assert_matches!(&contract.protocol_state, ProtocolContractState::Running(_));
assert!(contract.parameters_votes.is_empty());
}

#[test]
fn vote_add_domains__should_clear_pending_parameter_votes_on_transition() {
// Given
let (mut contract, participants, signer, domain_id) = setup_running_contract_with_domain(
3,
GovernanceThreshold::new(2),
ReconstructionThreshold::new(2),
);
let proposal = ProposedGovernanceThresholdParameters::new(
GovernanceThresholdParameters::new(participants.clone(), GovernanceThreshold::new(2))
.unwrap(),
BTreeMap::new(),
);
vote_params(&mut contract, &signer, &proposal).unwrap();
assert!(!contract.parameters_votes.is_empty());
let new_domain = dtos::DomainConfig {
id: DomainId(domain_id.0 + 1),
protocol: Protocol::CaitSith,
reconstruction_threshold: ReconstructionThreshold::new(2),
purpose: DomainPurpose::Sign,
};

// When
let mut env = Environment::new(Some(0), None, None);
for (account_id, _, _) in participants.participants() {
env.set_signer(account_id);
contract.vote_add_domains(vec![new_domain.clone()]).unwrap();
}

// Then
assert_matches!(
&contract.protocol_state,
ProtocolContractState::Initializing(_)
);
assert!(contract.parameters_votes.is_empty());
}

/// Installs a context where `account_id` calls as an attested participant: `init_running`
/// mocks each participant's attestation with its TLS key as the account key.
fn attested_context(participants: &Participants, account_id: &AccountId) {
Comment thread
SimonRastikian marked this conversation as resolved.
let info = participants.info(account_id).unwrap();
Environment::new(Some(0), Some(account_id.clone()), None)
.set_pk(near_sdk::PublicKey::from(info.tls_public_key.clone()));
}

/// Drives a 3-participant Running contract with one domain into Resharing: every
/// participant votes `proposal`, a key refresh keeping participants and threshold.
fn resharing_contract_with_domain() -> (
MpcContract,
Participants,
DomainId,
ProposedGovernanceThresholdParameters,
) {
let (mut contract, participants, _, domain_id) = setup_running_contract_with_domain(
3,
GovernanceThreshold::new(2),
ReconstructionThreshold::new(2),
);
let proposal = ProposedGovernanceThresholdParameters::new(
GovernanceThresholdParameters::new(participants.clone(), GovernanceThreshold::new(2))
.unwrap(),
BTreeMap::new(),
);
for (account_id, _, _) in participants.participants() {
vote_params(&mut contract, account_id, &proposal).unwrap();
}
assert_matches!(
&contract.protocol_state,
ProtocolContractState::Resharing(_)
);
(contract, participants, domain_id, proposal)
}

#[test]
fn vote_new_parameters__should_record_a_re_proposal_vote_while_resharing() {
// Given
let (mut contract, participants, _, proposal) = resharing_contract_with_domain();
let (voter, _, _) = &participants.participants()[0];
Environment::new(Some(0), Some(voter.clone()), None);

// When: a participant re-proposes at the next prospective epoch
let result =
contract.vote_new_parameters(dtos::EpochId::new(2), (&proposal).into_dto_type());

// Then
assert_matches!(result, Ok(()));
assert_matches!(
&contract.protocol_state,
ProtocolContractState::Running(state)
if state.parameters_votes == GovernanceThresholdParametersVotes::default()
ProtocolContractState::Resharing(_)
);
assert!(!contract.parameters_votes.is_empty());
}

#[test]
fn vote_reshared__should_clear_pending_parameter_votes_on_transition() {
// Given: a Resharing contract holding a pending re-proposal vote
let (mut contract, participants, domain_id, proposal) = resharing_contract_with_domain();
let voter = authenticate_account_as(&participants.participants()[0].0, &participants);
contract.parameters_votes.vote(&proposal, voter);
assert!(!contract.parameters_votes.is_empty());

// When: the resharing completes
let ProtocolContractState::Resharing(resharing) = &contract.protocol_state else {
panic!("expected Resharing state");
};
let (leader, _) = find_leader(&resharing.resharing_key);
let key_event_id = KeyEventId {
attempt_id: AttemptId::new(),
domain_id,
epoch_id: EpochId::new(1),
};
attested_context(&participants, &leader);
contract.start_reshare_instance(key_event_id).unwrap();
for (account_id, _, _) in participants.participants() {
attested_context(&participants, account_id);
contract.vote_reshared(key_event_id).unwrap();
}

// Then
assert_matches!(&contract.protocol_state, ProtocolContractState::Running(_));
assert!(contract.parameters_votes.is_empty());
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/contract/src/api/key_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ impl MpcContract {
if let Some(new_state) = self.protocol_state.vote_reshared(key_event_id)? {
// Resharing has concluded, transition to running state
self.protocol_state = new_state;
self.parameters_votes.clear();
self.recompute_available_foreign_chains();

// Spawn a promise to clean up votes from non-participants.
Expand Down
3 changes: 3 additions & 0 deletions crates/contract/src/api/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::primitives::domain::{
AddDomainsVotes, DomainRegistry, validate_domains_against_governance,
};
use crate::primitives::key_state::{EpochId, Keyset};
use crate::primitives::threshold_votes::GovernanceThresholdParametersVotes;
use crate::primitives::thresholds::GovernanceThresholdParameters;
use crate::state::ProtocolContractState;
use crate::state::running::RunningContractState;
Expand Down Expand Up @@ -75,6 +76,7 @@ impl MpcContract {
tee_verifier_account_id,
tee_verifier_votes: TeeVerifierVotes::default(),
available_attestation_grants: IterableMap::new(StorageKey::AttestationGrants),
parameters_votes: GovernanceThresholdParametersVotes::default(),
})
}

Expand Down Expand Up @@ -154,6 +156,7 @@ impl MpcContract {
tee_verifier_account_id,
tee_verifier_votes: TeeVerifierVotes::default(),
available_attestation_grants: IterableMap::new(StorageKey::AttestationGrants),
parameters_votes: GovernanceThresholdParametersVotes::default(),
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/contract/src/api/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ impl MpcContract {
tee_verifier_account_id: bogus_tee_verifier_account_id(),
tee_verifier_votes: Default::default(),
available_attestation_grants: IterableMap::new(StorageKey::AttestationGrants),
parameters_votes: Default::default(),
}
}
}
Expand Down
22 changes: 5 additions & 17 deletions crates/contract/src/dto_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use mpc_attestation::{
collateral::Collateral,
};
use near_mpc_contract_interface::types as dtos;
use std::collections::BTreeMap;

use crate::{
config::Config,
Expand All @@ -23,7 +24,6 @@ use crate::{
domain::{AddDomainsVotes, DomainRegistry},
key_state::{AuthenticatedAccountId, AuthenticatedParticipantId, KeyForDomain, Keyset},
participants::{ParticipantInfo, Participants},
threshold_votes::GovernanceThresholdParametersVotes,
thresholds::{GovernanceThresholdParameters, ProposedGovernanceThresholdParameters},
votes::Votes,
},
Expand Down Expand Up @@ -804,21 +804,6 @@ impl IntoInterfaceType<dtos::ProposedGovernanceThresholdParameters>

// --- Voting types ---

impl IntoInterfaceType<dtos::GovernanceThresholdParametersVotes>
for &GovernanceThresholdParametersVotes
{
fn into_dto_type(self) -> dtos::GovernanceThresholdParametersVotes {
let proposal_by_account = self
.proposal_by_account
.iter()
.map(|(account, params)| (account.into_dto_type(), params.into_dto_type()))
.collect();
dtos::GovernanceThresholdParametersVotes {
proposal_by_account,
}
}
}

impl IntoInterfaceType<dtos::AddDomainsVotes> for &AddDomainsVotes {
fn into_dto_type(self) -> dtos::AddDomainsVotes {
dtos::AddDomainsVotes {
Expand Down Expand Up @@ -979,7 +964,10 @@ impl IntoInterfaceType<dtos::RunningContractState> for &RunningContractState {
domains: (&self.domains).into_dto_type(),
keyset: (&self.keyset).into_dto_type(),
parameters: (&self.parameters).into_dto_type(),
parameters_votes: (&self.parameters_votes).into_dto_type(),
// TODO(#2825): reporting dropped ahead of the switch to hash-only votes.
parameters_votes: dtos::GovernanceThresholdParametersVotes {
proposal_by_account: BTreeMap::new(),
},
add_domains_votes: (&self.add_domains_votes).into_dto_type(),
previously_cancelled_resharing_epoch_id: self.previously_cancelled_resharing_epoch_id,
}
Expand Down
7 changes: 6 additions & 1 deletion crates/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ mod sandbox_test_methods;
pub use crate::pending_requests::MAX_PENDING_REQUEST_FAN_OUT;

use crate::{
foreign_chains_metadata::ForeignChainsMetadata, tee::tee_state::TeeState,
foreign_chains_metadata::ForeignChainsMetadata,
primitives::threshold_votes::GovernanceThresholdParametersVotes, tee::tee_state::TeeState,
tee::verifier_votes::TeeVerifierVotes, update::ContractUpdateVotes,
};
use config::Config;
Expand Down Expand Up @@ -66,6 +67,10 @@ pub struct MpcContract {
tee_verifier_votes: TeeVerifierVotes,
/// A row is removed at zero, so the map holds no entry for an account with none.
available_attestation_grants: IterableMap<AccountId, u32>,
// TODO(#2825): make these storage-backed `Votes`, which is why they live outside the state.
/// Pending votes for new parameters; emptied on every protocol state transition except a
/// resharing cancellation.
parameters_votes: GovernanceThresholdParametersVotes,
}

impl Default for MpcContract {
Expand Down
14 changes: 11 additions & 3 deletions crates/contract/src/primitives/threshold_votes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ use std::collections::BTreeMap;

/// Tracks votes for proposed threshold parameters (new participants, threshold,
/// and per-domain threshold updates). Each current participant can maintain one vote.
// TODO(#2825): Replace with Votes<AuthenticatedAccountId> from votes.rs
// once this type is moved out of RunningContractState (which requires Clone + PartialEq).
// TODO(#2825): Replace with Votes<AuthenticatedAccountId> from votes.rs.
#[near(serializers=[borsh])]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Debug, Default)]
pub struct GovernanceThresholdParametersVotes {
pub(crate) proposal_by_account:
BTreeMap<AuthenticatedAccountId, ProposedGovernanceThresholdParameters>,
Expand Down Expand Up @@ -52,6 +51,15 @@ impl GovernanceThresholdParametersVotes {
.count(),
)
}

pub fn clear(&mut self) {
self.proposal_by_account.clear();
}

#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.proposal_by_account.is_empty()
}
}

#[cfg(test)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,10 @@ BorshSchemaContainer {
"available_attestation_grants",
"IterableMap",
),
(
"parameters_votes",
"GovernanceThresholdParametersVotes",
),
],
),
},
Expand Down Expand Up @@ -942,10 +946,6 @@ BorshSchemaContainer {
"parameters",
"GovernanceThresholdParameters",
),
(
"parameters_votes",
"GovernanceThresholdParametersVotes",
),
(
"add_domains_votes",
"AddDomainsVotes",
Expand Down
Loading
Loading