Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 14 additions & 4 deletions crates/backup-cli/src/adapters/contract_state_fixture.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::path::{Path, PathBuf};

use near_mpc_contract_interface::types::{Keyset, ProtocolContractState};
use near_mpc_contract_interface::types::{
Keyset, ProtocolContractState, ProtocolContractStateCompat,
};
use tokio::fs::File;
use tokio::io::AsyncReadExt;

Expand Down Expand Up @@ -49,7 +51,12 @@ impl ContractStateReader for ContractStateFixture {
.await
.map_err(Error::Read)?;

serde_json::from_slice(&buffer).map_err(Error::JsonDeserialization)
// TODO(XXXX): Switch to canonical after upgrade 3.14.0
// `state()` still emits the pre-3903 field names; deserialize the compat
// shape and convert to the canonical DTO.
let state: ProtocolContractStateCompat =
serde_json::from_slice(&buffer).map_err(Error::JsonDeserialization)?;
Ok(state.into())
}
}

Expand All @@ -75,7 +82,7 @@ pub fn get_keyset_from_contract_state(
mod tests {
use std::path::PathBuf;

use near_mpc_contract_interface::types::{ProtocolContractState, Threshold};
use near_mpc_contract_interface::types::{GovernanceThreshold, ProtocolContractState};

use crate::{
adapters::contract_state_fixture::ContractStateFixture, ports::ContractStateReader,
Expand All @@ -95,7 +102,10 @@ mod tests {
let ProtocolContractState::Running(state) = &contract_state else {
panic!("expected Running state, got {contract_state:?}");
};
assert_eq!(state.parameters.threshold, Threshold::new(7));
assert_eq!(
state.parameters.governance_threshold,
GovernanceThreshold::new(7)
);
assert_eq!(state.domains.domains.len(), 2);
}
}
150 changes: 112 additions & 38 deletions crates/contract/src/dto_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use crate::{
domain::{AddDomainsVotes, DomainRegistry},
key_state::{AuthenticatedAccountId, AuthenticatedParticipantId, KeyForDomain, Keyset},
participants::{ParticipantInfo, Participants},
threshold_votes::ThresholdParametersVotes,
thresholds::{ProposedThresholdParameters, ThresholdParameters},
threshold_votes::GovernanceThresholdParametersVotes,
thresholds::{GovernanceThresholdParameters, ProposedGovernanceThresholdParameters},
},
state::{
ProtocolContractState,
Expand Down Expand Up @@ -214,21 +214,27 @@ impl IntoContractType<Participants> for dtos::Participants {
}
}

impl TryIntoContractType<ThresholdParameters> for dtos::ThresholdParameters {
// TODO(XXXX): Switch to canonical after upgrade 3.14.0
impl TryIntoContractType<GovernanceThresholdParameters>
for dtos::GovernanceThresholdParametersCompat
{
type Error = Error;

fn try_into_contract_type(self) -> Result<ThresholdParameters, Self::Error> {
fn try_into_contract_type(self) -> Result<GovernanceThresholdParameters, Self::Error> {
// Validate eagerly at the DTO boundary so invalid proposal parameters are rejected here.
ThresholdParameters::new(self.participants.into_contract_type(), self.threshold)
GovernanceThresholdParameters::new(self.participants.into_contract_type(), self.threshold)
}
}

impl TryIntoContractType<ProposedThresholdParameters> for dtos::ProposedThresholdParameters {
// TODO(XXXX): Switch to canonical after upgrade 3.14.0
impl TryIntoContractType<ProposedGovernanceThresholdParameters>
for dtos::ProposedGovernanceThresholdParametersCompat
{
type Error = Error;

fn try_into_contract_type(self) -> Result<ProposedThresholdParameters, Self::Error> {
fn try_into_contract_type(self) -> Result<ProposedGovernanceThresholdParameters, Self::Error> {
// Validates the inner threshold parameters; see the conversion above.
Ok(ProposedThresholdParameters::new(
Ok(ProposedGovernanceThresholdParameters::new(
self.parameters.try_into_contract_type()?,
self.per_domain_thresholds,
))
Expand Down Expand Up @@ -584,14 +590,30 @@ mod test_conversions {
}
}

impl From<ThresholdParameters> for dtos::ThresholdParameters {
fn from(params: ThresholdParameters) -> Self {
impl From<GovernanceThresholdParameters> for dtos::GovernanceThresholdParameters {
fn from(params: GovernanceThresholdParameters) -> Self {
(&params).into_dto_type()
}
}

impl From<ProposedThresholdParameters> for dtos::ProposedThresholdParameters {
fn from(params: ProposedThresholdParameters) -> Self {
impl From<ProposedGovernanceThresholdParameters> for dtos::ProposedGovernanceThresholdParameters {
fn from(params: ProposedGovernanceThresholdParameters) -> Self {
(&params).into_dto_type()
}
}

// TODO(XXXX): Delete this code after upgrade 3.14.0
impl From<GovernanceThresholdParameters> for dtos::GovernanceThresholdParametersCompat {
fn from(params: GovernanceThresholdParameters) -> Self {
(&params).into_dto_type()
}
}

// TODO(XXXX): Delete this code after upgrade 3.14.0
impl From<ProposedGovernanceThresholdParameters>
for dtos::ProposedGovernanceThresholdParametersCompat
{
fn from(params: ProposedGovernanceThresholdParameters) -> Self {
(&params).into_dto_type()
}
}
Expand Down Expand Up @@ -686,34 +708,78 @@ impl IntoInterfaceType<dtos::Participants> for &Participants {
}
}

impl IntoInterfaceType<dtos::ThresholdParameters> for &ThresholdParameters {
fn into_dto_type(self) -> dtos::ThresholdParameters {
dtos::ThresholdParameters {
impl IntoInterfaceType<dtos::GovernanceThresholdParameters> for &GovernanceThresholdParameters {
fn into_dto_type(self) -> dtos::GovernanceThresholdParameters {
dtos::GovernanceThresholdParameters {
participants: self.participants().into_dto_type(),
threshold: self.threshold(),
governance_threshold: self.threshold(),
}
}
}

impl IntoInterfaceType<dtos::ProposedThresholdParameters> for &ProposedThresholdParameters {
fn into_dto_type(self) -> dtos::ProposedThresholdParameters {
dtos::ProposedThresholdParameters {
impl IntoInterfaceType<dtos::ProposedGovernanceThresholdParameters>
for &ProposedGovernanceThresholdParameters
{
fn into_dto_type(self) -> dtos::ProposedGovernanceThresholdParameters {
dtos::ProposedGovernanceThresholdParameters {
parameters: self.parameters().into_dto_type(),
per_domain_thresholds: self.per_domain_thresholds().clone(),
per_domain_reconstruction_thresholds: self.per_domain_thresholds().clone(),
}
}
}

// --- Voting types ---

impl IntoInterfaceType<dtos::ThresholdParametersVotes> for &ThresholdParametersVotes {
fn into_dto_type(self) -> dtos::ThresholdParametersVotes {
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::ThresholdParametersVotes {
dtos::GovernanceThresholdParametersVotes {
proposal_by_account,
}
}
}

// TODO(XXXX): Delete this code after upgrade 3.14.0
impl IntoInterfaceType<dtos::GovernanceThresholdParametersCompat>
for &GovernanceThresholdParameters
{
fn into_dto_type(self) -> dtos::GovernanceThresholdParametersCompat {
dtos::GovernanceThresholdParametersCompat {
participants: self.participants().into_dto_type(),
threshold: self.threshold(),
}
}
}

// TODO(XXXX): Delete this code after upgrade 3.14.0
impl IntoInterfaceType<dtos::ProposedGovernanceThresholdParametersCompat>
for &ProposedGovernanceThresholdParameters
{
fn into_dto_type(self) -> dtos::ProposedGovernanceThresholdParametersCompat {
dtos::ProposedGovernanceThresholdParametersCompat {
parameters: self.parameters().into_dto_type(),
per_domain_thresholds: self.per_domain_thresholds().clone(),
}
}
}

// TODO(XXXX): Delete this code after upgrade 3.14.0
impl IntoInterfaceType<dtos::GovernanceThresholdParametersVotesCompat>
for &GovernanceThresholdParametersVotes
{
fn into_dto_type(self) -> dtos::GovernanceThresholdParametersVotesCompat {
let proposal_by_account = self
.proposal_by_account
.iter()
.map(|(account, params)| (account.into_dto_type(), params.into_dto_type()))
.collect();
dtos::GovernanceThresholdParametersVotesCompat {
proposal_by_account,
}
}
Expand Down Expand Up @@ -828,7 +894,7 @@ impl IntoInterfaceType<dtos::ResharingContractState> for &ResharingContractState
.iter()
.map(|a| a.into_dto_type())
.collect(),
per_domain_thresholds: self.per_domain_thresholds.clone(),
per_domain_reconstruction_thresholds: self.per_domain_thresholds.clone(),
}
}
}
Expand Down Expand Up @@ -868,7 +934,7 @@ mod tests {
use super::*;
use crate::errors::InvalidThreshold;
use crate::primitives::test_utils::gen_participants;
use crate::primitives::thresholds::Threshold;
use crate::primitives::thresholds::GovernanceThreshold;
use assert_matches::assert_matches;

const TEST_THRESHOLD: u64 = 2;
Expand Down Expand Up @@ -921,34 +987,42 @@ mod tests {
}

/// Ensures that the JSON produced by serializing the internal
/// [`ThresholdParameters`] type can be deserialized into the DTO
/// [`dtos::ThresholdParameters`] type and vice versa, producing identical
/// [`GovernanceThresholdParameters`] type can be deserialized into the DTO
/// [`dtos::GovernanceThresholdParametersCompat`] type and vice versa, producing identical
/// JSON in both directions.
#[test]
fn threshold_parameters_serde_is_compatible_with_dto() {
let internal =
ThresholdParameters::new(test_participants(), Threshold::new(TEST_THRESHOLD)).unwrap();
let internal = GovernanceThresholdParameters::new(
test_participants(),
GovernanceThreshold::new(TEST_THRESHOLD),
)
.unwrap();
let json = serde_json::to_value(&internal).unwrap();

let dto: dtos::ThresholdParameters = serde_json::from_value(json.clone()).unwrap();
let dto: dtos::GovernanceThresholdParametersCompat =
serde_json::from_value(json.clone()).unwrap();

let dto_json = serde_json::to_value(&dto).unwrap();
assert_eq!(json, dto_json, "Internal and DTO JSON must be identical");

let roundtrip: ThresholdParameters = serde_json::from_value(dto_json).unwrap();
let roundtrip: GovernanceThresholdParameters = serde_json::from_value(dto_json).unwrap();
assert_eq!(internal, roundtrip);
}

/// Verify that [`IntoInterfaceType::into_dto_type`] produces a DTO whose
/// serialization matches the internal type's serialization.
#[test]
fn into_dto_type_preserves_serialization() {
let internal =
ThresholdParameters::new(test_participants(), Threshold::new(TEST_THRESHOLD)).unwrap();
let internal = GovernanceThresholdParameters::new(
test_participants(),
GovernanceThreshold::new(TEST_THRESHOLD),
)
.unwrap();
let internal_json = serde_json::to_value(&internal).unwrap();

let dto: dtos::ThresholdParameters = (&internal).into_dto_type();
let dto_json = serde_json::to_value(&dto).unwrap();
let dto: dtos::GovernanceThresholdParameters = (&internal).into_dto_type();
let dto_compat: dtos::GovernanceThresholdParametersCompat = dto.into();
let dto_json = serde_json::to_value(&dto_compat).unwrap();

assert_eq!(internal_json, dto_json);
}
Expand All @@ -958,13 +1032,13 @@ mod tests {
#[test]
fn try_into_contract_type__should_reject_threshold_below_relative_requirement() {
// Given a DTO with 5 participants and a threshold of 2 (below the 60% bound of 3).
let dto = dtos::ThresholdParameters {
let dto = dtos::GovernanceThresholdParametersCompat {
participants: (&gen_participants(5)).into_dto_type(),
threshold: Threshold::new(2),
threshold: GovernanceThreshold::new(2),
};

// When converting the DTO into the contract type.
let result: Result<ThresholdParameters, Error> = dto.try_into_contract_type();
let result: Result<GovernanceThresholdParameters, Error> = dto.try_into_contract_type();

// Then conversion fails with the relative-threshold error.
assert_matches!(
Expand Down
4 changes: 2 additions & 2 deletions crates/contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,13 @@ pub enum InvalidState {

#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
pub enum InvalidThreshold {
#[error("Threshold does not meet the minimum absolute requirement")]
#[error("GovernanceThreshold does not meet the minimum absolute requirement")]
MinAbsRequirementFailed,
#[error(
"GovernanceThreshold is below the minimum required relative to the participant count: require at least {required}, found {found}"
)]
MinRelRequirementFailed { required: u64, found: u64 },
#[error("Threshold must not exceed number of participants: max {max}, found {found}")]
#[error("GovernanceThreshold must not exceed number of participants: max {max}, found {found}")]
MaxRequirementFailed { max: u64, found: u64 },
#[error(
"GovernanceThreshold exceeds the maximum allowed relative to the participant count: max {max}, found {found}"
Expand Down
17 changes: 10 additions & 7 deletions crates/contract/src/foreign_chain_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use near_sdk::near;
use near_sdk::store::IterableMap;

use crate::errors::{ChainEntryValidationError, ConversionError, Error, InvalidParameters};
use crate::primitives::thresholds::ThresholdParameters;
use crate::primitives::thresholds::GovernanceThresholdParameters;
use crate::primitives::votes::{ProposalHash, ProposalHashEncoding, Votes};
use crate::primitives::{key_state::AuthenticatedParticipantId, participants::Participants};
use crate::storage_keys::StorageKey;
Expand Down Expand Up @@ -174,7 +174,7 @@ impl ProviderVotes {
chain: ForeignChain,
hash: ProposalHash,
participant: AuthenticatedParticipantId,
threshold_parameters: &ThresholdParameters,
threshold_parameters: &GovernanceThresholdParameters,
) -> Result<bool, Error> {
let protocol_threshold = threshold_parameters.threshold().value();
let participants = threshold_parameters.participants();
Expand Down Expand Up @@ -223,7 +223,7 @@ impl ForeignChainRpcWhitelist {
&mut self,
participant: AuthenticatedParticipantId,
votes: NonEmptyBTreeMap<ForeignChain, dtos::ChainEntry>,
threshold_parameters: &ThresholdParameters,
threshold_parameters: &GovernanceThresholdParameters,
) -> Result<Vec<ForeignChain>, Error> {
let mut applied: Vec<ForeignChain> = Vec::new();
let votes: BTreeMap<ForeignChain, dtos::ChainEntry> = votes.into();
Expand All @@ -250,15 +250,18 @@ mod tests {
key_state::AuthenticatedParticipantId, test_utils::gen_authenticated_participants,
};
use assert_matches::assert_matches;
use mpc_primitives::Threshold;
use mpc_primitives::GovernanceThreshold;
use near_mpc_contract_interface::types::AuthScheme;

/// Build a `ThresholdParameters` for tests, bypassing the relative-threshold
/// Build a `GovernanceThresholdParameters` for tests, bypassing the relative-threshold
/// validation so tests can express edge-case combinations (e.g. the stale-votes
/// test deliberately uses a threshold > current participant count to assert
/// the count_for predicate filters out non-participant rows).
fn tp(participants: &Participants, n: u64) -> ThresholdParameters {
ThresholdParameters::new_unvalidated(participants.clone(), Threshold::new(n))
fn tp(participants: &Participants, n: u64) -> GovernanceThresholdParameters {
GovernanceThresholdParameters::new_unvalidated(
participants.clone(),
GovernanceThreshold::new(n),
)
}

fn provider(id: &str) -> (ProviderId, ProviderConfig) {
Expand Down
Loading