diff --git a/crates/backup-cli/assets/contract_state.json b/crates/backup-cli/assets/contract_state.json index e36b5e9a0a..bd1073643c 100644 --- a/crates/backup-cli/assets/contract_state.json +++ b/crates/backup-cli/assets/contract_state.json @@ -164,9 +164,6 @@ }, "threshold": 7 }, - "parameters_votes": { - "proposal_by_account": {} - }, "add_domains_votes": { "proposal_by_account": {} } diff --git a/crates/contract/README.md b/crates/contract/README.md index b81736e24f..4d11257b32 100644 --- a/crates/contract/README.md +++ b/crates/contract/README.md @@ -345,6 +345,7 @@ These functions require the caller to be a participant or candidate. | `allowed_os_measurements()` | Returns all currently allowed OS measurement sets. | `Vec` | TBD | TBD | | `os_measurement_votes()` | Returns pending OS measurement votes, keyed by the proposal hash of the action voted for, with the accounts backing each; may include former participants' votes. | `MeasurementVotes` | TBD | TBD | | `contract_update_votes()` | Returns current update votes, keyed by the hash voted for, with the accounts backing each. | `BTreeMap>` | TBD | TBD | +| `parameters_votes()` | Returns pending `vote_new_parameters` votes, keyed by the proposal hash of the `ProposedGovernanceThresholdParameters` voted for, with the accounts backing each. | `GovernanceThresholdParametersVotes` | TBD | TBD | | `clean_tee_status()` | Private endpoint. Cleans up TEE information for non-participants after resharing. Only callable by the contract itself via a promise. | `Result<(), Error>` | TBD | TBD | ## Building the contract diff --git a/crates/contract/src/api/governance.rs b/crates/contract/src/api/governance.rs index 0b74cc48ef..0d8ad7b6e5 100644 --- a/crates/contract/src/api/governance.rs +++ b/crates/contract/src/api/governance.rs @@ -1,6 +1,6 @@ //! Governance votes over the participant set and the domain registry. -use crate::dto_mapping::TryIntoContractType; +use crate::dto_mapping::{IntoInterfaceType, TryIntoContractType}; use crate::errors::{Error, InvalidParameters}; use crate::primitives::thresholds::{GovernanceThreshold, ProposedGovernanceThresholdParameters}; use crate::tee::tee_state::TeeValidationResult; @@ -104,6 +104,11 @@ impl MpcContract { } Ok(()) } + + /// Returns the pending `vote_new_parameters` votes, keyed by proposal hash. + pub fn parameters_votes(&self) -> dtos::GovernanceThresholdParametersVotes { + (&self.parameters_votes).into_dto_type() + } } impl MpcContract { @@ -121,10 +126,10 @@ mod tests { forwarded_participant_call_contract, make_public_key_for_curve, setup_tee_test_contract, submit_attestation, submit_valid_attestations, }; - use crate::dto_mapping::IntoInterfaceType; use crate::errors::{DomainError, InvalidCandidateSet, InvalidState, InvalidThreshold}; use crate::primitives::key_state::{AttemptId, EpochId, KeyEventId, KeyForDomain, Keyset}; use crate::primitives::participants::Participants; + use crate::primitives::proposal_hash::ToProposalHash; use crate::primitives::test_utils::{ authenticate_account_as, bogus_tee_verifier_account_id, gen_participants, }; @@ -564,6 +569,62 @@ mod tests { (contract, participants, domain_id, proposal) } + /// The tally for `voters` all voting `proposal`. + fn expected_tally( + proposal: &ProposedGovernanceThresholdParameters, + voters: &[&AccountId], + ) -> dtos::GovernanceThresholdParametersVotes { + dtos::GovernanceThresholdParametersVotes(BTreeMap::from([( + proposal.to_proposal_hash(), + voters + .iter() + .map(|voter| dtos::AuthenticatedAccountId((*voter).clone())) + .collect(), + )])) + } + + #[test] + fn parameters_votes__should_report_pending_votes_by_proposal_hash() { + // Given + let (mut contract, participants, signer, _) = setup_running_contract_with_domain( + 3, + GovernanceThreshold::new(2), + ReconstructionThreshold::new(2), + ); + let proposal = ProposedGovernanceThresholdParameters::new( + GovernanceThresholdParameters::new(participants, GovernanceThreshold::new(2)).unwrap(), + BTreeMap::new(), + ); + + // When + vote_params(&mut contract, &signer, &proposal).unwrap(); + + // Then + assert_eq!( + contract.parameters_votes(), + expected_tally(&proposal, &[&signer]) + ); + } + + #[test] + fn parameters_votes__should_report_re_proposal_votes_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 + contract + .vote_new_parameters(dtos::EpochId::new(2), (&proposal).into_dto_type()) + .unwrap(); + + // Then + assert_eq!( + contract.parameters_votes(), + expected_tally(&proposal, &[voter]) + ); + } + #[test] fn vote_new_parameters__should_record_a_re_proposal_vote_while_resharing() { // Given diff --git a/crates/contract/src/dto_mapping.rs b/crates/contract/src/dto_mapping.rs index 0b77381fc3..6550507fc7 100644 --- a/crates/contract/src/dto_mapping.rs +++ b/crates/contract/src/dto_mapping.rs @@ -14,7 +14,6 @@ use mpc_attestation::{ collateral::Collateral, }; use near_mpc_contract_interface::types as dtos; -use std::collections::BTreeMap; use crate::{ config::Config, @@ -24,6 +23,7 @@ use crate::{ domain::{AddDomainsVotes, DomainRegistry}, key_state::{AuthenticatedAccountId, AuthenticatedParticipantId, KeyForDomain, Keyset}, participants::{ParticipantInfo, Participants}, + threshold_votes::GovernanceThresholdParametersVotes, thresholds::{GovernanceThresholdParameters, ProposedGovernanceThresholdParameters}, votes::Votes, }, @@ -816,6 +816,21 @@ impl IntoInterfaceType for &AddDomainsVotes { } } +impl IntoInterfaceType + for &GovernanceThresholdParametersVotes +{ + fn into_dto_type(self) -> dtos::GovernanceThresholdParametersVotes { + dtos::GovernanceThresholdParametersVotes( + self.all() + .into_iter() + .map(|(proposal, voters)| { + (proposal, voters.iter().map(|v| v.into_dto_type()).collect()) + }) + .collect(), + ) + } +} + impl IntoInterfaceType for &Votes { fn into_dto_type(self) -> dtos::MeasurementVotes { dtos::MeasurementVotes( @@ -964,9 +979,6 @@ impl IntoInterfaceType for &RunningContractState { domains: (&self.domains).into_dto_type(), keyset: (&self.keyset).into_dto_type(), parameters: (&self.parameters).into_dto_type(), - 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, } diff --git a/crates/contract/src/primitives/threshold_votes.rs b/crates/contract/src/primitives/threshold_votes.rs index d37d73b545..be2be8f6db 100644 --- a/crates/contract/src/primitives/threshold_votes.rs +++ b/crates/contract/src/primitives/threshold_votes.rs @@ -1,4 +1,4 @@ -use crate::primitives::proposal_hash::{Borsh, Sha256, ToProposalHash}; +use crate::primitives::proposal_hash::{Borsh, ProposalHash, Sha256, ToProposalHash}; use crate::primitives::thresholds::ProposedGovernanceThresholdParameters; use crate::primitives::votes::Votes; use crate::primitives::{ @@ -7,6 +7,7 @@ use crate::primitives::{ }; use crate::storage_keys::StorageKey; use near_sdk::near; +use std::collections::{BTreeMap, BTreeSet}; impl ToProposalHash for ProposedGovernanceThresholdParameters { type Serializer = Borsh; @@ -60,6 +61,10 @@ impl GovernanceThresholdParametersVotes { self.0.clear(); } + pub fn all(&self) -> BTreeMap> { + self.0.all() + } + #[cfg(test)] pub fn is_empty(&self) -> bool { self.0.all().is_empty() 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 051dbe8138..b29cbb4b1e 100644 --- a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap +++ b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap @@ -635,6 +635,24 @@ expression: abi } } }, + { + "name": "parameters_votes", + "doc": " Returns the pending `vote_new_parameters` votes, keyed by proposal hash.", + "kind": "view", + "result": { + "serialization_type": "json", + "type_schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/AuthenticatedAccountId" + }, + "uniqueItems": true + } + } + } + }, { "name": "prepay_attestation_storage", "doc": " Buys `grants` attestation-storage grants for `account_id`.\n\n Permissionless, which is what lets an operator fund a node whose function-call key\n cannot attach a deposit. Requires exactly the fee times `grants`; no refunds, no\n withdrawal.", @@ -3716,21 +3734,6 @@ expression: abi } } }, - "GovernanceThresholdParametersVotes": { - "description": "Votes for threshold parameter changes. Always empty: the contract stores only proposal hashes and no longer reports pending votes. Kept for wire compatibility.", - "type": "object", - "required": [ - "proposal_by_account" - ], - "properties": { - "proposal_by_account": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProposedGovernanceThresholdParameters" - } - } - } - }, "Hash256": { "type": "string", "pattern": "^(?:[0-9A-Fa-f]{2})*$" @@ -4765,8 +4768,7 @@ expression: abi "add_domains_votes", "domains", "keyset", - "parameters", - "parameters_votes" + "parameters" ], "properties": { "add_domains_votes": { @@ -4781,9 +4783,6 @@ expression: abi "parameters": { "$ref": "#/definitions/GovernanceThresholdParameters" }, - "parameters_votes": { - "$ref": "#/definitions/GovernanceThresholdParametersVotes" - }, "previously_cancelled_resharing_epoch_id": { "anyOf": [ { diff --git a/crates/near-mpc-contract-interface/src/client.rs b/crates/near-mpc-contract-interface/src/client.rs index be9aab7f1e..5da1160641 100644 --- a/crates/near-mpc-contract-interface/src/client.rs +++ b/crates/near-mpc-contract-interface/src/client.rs @@ -34,8 +34,8 @@ use crate::method_names::{ GET_FOREIGN_CHAINS_CONFIGS, GET_PENDING_CKD_REQUEST, GET_PENDING_REQUEST, GET_PENDING_VERIFY_FOREIGN_TX_REQUEST, GET_TEE_ACCOUNTS, INIT, INIT_RUNNING, LAUNCHER_HASH_VOTES, MIGRATE, MIGRATION_INFO, MPC_NODE_MANIFEST_DIGEST_VOTES, - OS_MEASUREMENT_VOTES, PREPAY_ATTESTATION_STORAGE, PUBLIC_KEY, REGISTER_BACKUP_SERVICE, - REGISTER_FOREIGN_CHAINS_CONFIG, REMOVE_CONTRACT_UPDATE_VOTE, + OS_MEASUREMENT_VOTES, PARAMETERS_VOTES, PREPAY_ATTESTATION_STORAGE, PUBLIC_KEY, + REGISTER_BACKUP_SERVICE, REGISTER_FOREIGN_CHAINS_CONFIG, REMOVE_CONTRACT_UPDATE_VOTE, REMOVE_NON_PARTICIPANT_CONTRACT_UPDATE_VOTES, REQUEST_APP_PRIVATE_KEY, RESPOND, RESPOND_CKD, RESPOND_VERIFY_FOREIGN_TX, SIGN, START_KEYGEN_INSTANCE, START_NODE_MIGRATION, START_RESHARE_INSTANCE, STATE, SUBMIT_CONTRACT_UPDATE, SUBMIT_PARTICIPANT_INFO, @@ -50,12 +50,12 @@ use crate::types::{ AvailableForeignChains, BackupServiceInfo, CKDAppPublicKey, CKDRequest, CKDRequestArgs, CKDResponse, ChainEntry, CodeHashesVotes, Config, DestinationNodeInfo, DomainConfig, DomainId, Ed25519PublicKey, EpochId, ExpectedMeasurements, ForeignChain, ForeignChainsConfig, - ForeignChainsConfigs, GovernanceThresholdParameters, InitConfig, KeyEventId, Keyset, - LauncherDockerComposeHash, LauncherHashVotes, LauncherImageHash, MeasurementVotes, - MigrationInfo, NodeId, NodeImageHash, ParticipantUrl, ProposalHash, - ProposedGovernanceThresholdParameters, ProtocolContractState, PublicKey, SignRequestArgs, - SignatureRequest, SignatureResponse, StoredAttestation, TeeVerifierCodeHash, Update, - UpdateHash, VerifyForeignTransactionRequest, VerifyForeignTransactionRequestArgs, + ForeignChainsConfigs, GovernanceThresholdParameters, GovernanceThresholdParametersVotes, + InitConfig, KeyEventId, Keyset, LauncherDockerComposeHash, LauncherHashVotes, + LauncherImageHash, MeasurementVotes, MigrationInfo, NodeId, NodeImageHash, ParticipantUrl, + ProposalHash, ProposedGovernanceThresholdParameters, ProtocolContractState, PublicKey, + SignRequestArgs, SignatureRequest, SignatureResponse, StoredAttestation, TeeVerifierCodeHash, + Update, UpdateHash, VerifyForeignTransactionRequest, VerifyForeignTransactionRequestArgs, VerifyForeignTransactionResponse, YieldIndex, }; use near_mpc_bounded_collections::NonEmptyBTreeMap; @@ -633,6 +633,10 @@ impl MpcContractHandle { self.view(ViewArgs::no_args(LAUNCHER_HASH_VOTES)) } + pub fn parameters_votes(&self) -> ViewCall { + self.view(ViewArgs::no_args(PARAMETERS_VOTES)) + } + pub fn os_measurement_votes(&self) -> ViewCall { self.view(ViewArgs::no_args(OS_MEASUREMENT_VOTES)) } @@ -1139,6 +1143,7 @@ mod tests { let _ = handle.allowed_os_measurements().await; let _ = handle.mpc_node_manifest_digest_votes().await; let _ = handle.launcher_hash_votes().await; + let _ = handle.parameters_votes().await; let _ = handle.os_measurement_votes().await; let _ = handle.contract_update_votes().await; let _ = handle.state().await; diff --git a/crates/near-mpc-contract-interface/src/method_names.rs b/crates/near-mpc-contract-interface/src/method_names.rs index 16e292e783..3713fb85c1 100644 --- a/crates/near-mpc-contract-interface/src/method_names.rs +++ b/crates/near-mpc-contract-interface/src/method_names.rs @@ -77,6 +77,7 @@ pub const GET_TEE_ACCOUNTS: &str = "get_tee_accounts"; pub const AVAILABLE_ATTESTATION_GRANTS: &str = "available_attestation_grants"; pub const TEE_VERIFIER_ACCOUNT_ID: &str = "tee_verifier_account_id"; pub const TEE_VERIFIER_VOTES: &str = "tee_verifier_votes"; +pub const PARAMETERS_VOTES: &str = "parameters_votes"; pub const GET_ATTESTATION: &str = "get_attestation"; pub const GET_AVAILABLE_FOREIGN_CHAINS: &str = "get_available_foreign_chains"; pub const GET_FOREIGN_CHAINS_CONFIGS: &str = "get_foreign_chains_configs"; diff --git a/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_view_catalog.snap b/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_view_catalog.snap index 79fdf2c393..206a5d6cd0 100644 --- a/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_view_catalog.snap +++ b/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_view_catalog.snap @@ -26,6 +26,10 @@ contract: mpc.near method: launcher_hash_votes args: {} +contract: mpc.near +method: parameters_votes +args: {} + contract: mpc.near method: os_measurement_votes args: {} diff --git a/crates/near-mpc-contract-interface/src/types/state.rs b/crates/near-mpc-contract-interface/src/types/state.rs index 94454fca38..b76a250279 100644 --- a/crates/near-mpc-contract-interface/src/types/state.rs +++ b/crates/near-mpc-contract-interface/src/types/state.rs @@ -7,6 +7,7 @@ use crate::types::PublicKeyExtended; use crate::types::participants::Participants; use crate::types::primitives::AccountId; use borsh::{BorshDeserialize, BorshSerialize}; +use mpc_primitives::hash::ProposalHash; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -167,18 +168,28 @@ pub struct ProposedGovernanceThresholdParameters { // Voting Types // ============================================================================= -/// Votes for threshold parameter changes. Always empty: the contract stores only proposal -/// hashes and no longer reports pending votes. Kept for wire compatibility. -// TODO(#2825): report the hash-keyed tally instead, like `MeasurementVotes`. -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// Voters grouped by the [`ProposalHash`] of the [`ProposedGovernanceThresholdParameters`] +/// they voted for. +#[derive( + Clone, + Debug, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + derive_more::Deref, +)] +#[serde(transparent)] #[cfg_attr( all(feature = "abi", not(target_arch = "wasm32")), derive(schemars::JsonSchema) )] -pub struct GovernanceThresholdParametersVotes { - pub proposal_by_account: - BTreeMap, -} +pub struct GovernanceThresholdParametersVotes( + pub BTreeMap>, +); /// Votes for adding new domains. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -250,7 +261,6 @@ pub struct RunningContractState { pub domains: DomainRegistry, pub keyset: Keyset, pub parameters: GovernanceThresholdParameters, - pub parameters_votes: GovernanceThresholdParametersVotes, pub add_domains_votes: AddDomainsVotes, pub previously_cancelled_resharing_epoch_id: Option, } @@ -503,4 +513,58 @@ mod tests { // Then assert_eq!(parsed.per_domain_thresholds.len(), 0); } + + #[test] + #[expect(non_snake_case)] + fn governance_threshold_parameters_votes__should_serialize_as_a_transparent_map_keyed_by_proposal_hash() + { + // Given + let proposal = ProposalHash::new([0xCD; 32]); + let votes = GovernanceThresholdParametersVotes(BTreeMap::from([( + proposal, + BTreeSet::from([AuthenticatedAccountId("alice.near".parse().unwrap())]), + )])); + + // When + let json = serde_json::to_value(&votes).unwrap(); + + // Then + assert_eq!(json[proposal.as_hex()][0], "alice.near"); + } + + // TODO(#4984): drop once no deployed contract reports the pre-tally voter-keyed shape. + #[test] + #[expect(non_snake_case)] + fn running_contract_state__should_ignore_the_legacy_parameters_votes_report() { + // Given: the `state()` of a 3.18 contract while a vote is pending + let parameters = GovernanceThresholdParameters { + participants: sample_participants(), + threshold: GovernanceThreshold::new(1), + }; + let state = RunningContractState { + domains: DomainRegistry { + domains: Vec::new(), + next_domain_id: DomainId(0), + }, + keyset: Keyset::new(EpochId::new(0), Vec::new()), + parameters: parameters.clone(), + add_domains_votes: AddDomainsVotes { + proposal_by_account: BTreeMap::new(), + }, + previously_cancelled_resharing_epoch_id: None, + }; + let mut json = serde_json::to_value(&state).unwrap(); + json["parameters_votes"] = serde_json::json!({ "proposal_by_account": { + "alice.near": ProposedGovernanceThresholdParameters { + parameters, + per_domain_thresholds: BTreeMap::new(), + }, + }}); + + // When + let parsed: RunningContractState = serde_json::from_value(json).unwrap(); + + // Then + assert_eq!(parsed, state); + } } diff --git a/docs/guide/running-an-mpc-node-in-tdx-external-guide/running-an-mpc-node-in-tdx-external-guide.md b/docs/guide/running-an-mpc-node-in-tdx-external-guide/running-an-mpc-node-in-tdx-external-guide.md index 7ba3ee525b..3ab9e80164 100644 --- a/docs/guide/running-an-mpc-node-in-tdx-external-guide/running-an-mpc-node-in-tdx-external-guide.md +++ b/docs/guide/running-an-mpc-node-in-tdx-external-guide/running-an-mpc-node-in-tdx-external-guide.md @@ -2455,6 +2455,19 @@ ERROR mpc_node::key_events: Key resharing attempt KeyEventId { ... } failed: