Skip to content

Commit

Permalink
chore: additional changes to propose_update (#729)
Browse files Browse the repository at this point in the history
* Made update methods us MpcContractError

* Made proposed updates more space efficient

* Added bytes_used for config and code updates

* Removed unnecessary near-rng crate

* Added deposit to propose_update

* Make config dynamic for easier changes

* Added proper borsh serialization and payable

* Use to_vec instead for config borsh ser

* Update deposit cost

* Clippy

* Added propose_update refund diff and migrate

* Added additional test case for a contract that fails to migrate

* Update INVALID contract path
  • Loading branch information
ChaoticTempest authored Jul 25, 2024
1 parent 2086c26 commit fa6fa9b
Show file tree
Hide file tree
Showing 14 changed files with 371 additions and 90 deletions.
7 changes: 0 additions & 7 deletions chain-signatures/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion chain-signatures/contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ schemars = "0.8"
k256 = { version = "0.13.1", features = ["sha256", "ecdsa", "serde", "arithmetic", "expose-field"] }
crypto-shared = { path = "../crypto-shared" }
near-gas = { version = "0.2.5", features = ["serde", "borsh", "schemars"] }
near-rng = "0.1.1"
thiserror = "1"

[dev-dependencies]
Expand Down
90 changes: 73 additions & 17 deletions chain-signatures/contract/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
use std::collections::HashMap;

use borsh::{self, BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use near_sdk::serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
#[derive(
Clone, Default, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq,
)]
pub struct Config {
/// Timeout for triple generation in milliseconds.
pub triple_timeout: u64,
/// Timeout for presignature generation in milliseconds.
pub presignature_timeout: u64,
/// Timeout for signature generation in milliseconds.
pub signature_timeout: u64,
}

impl Default for Config {
fn default() -> Self {
Self {
triple_timeout: min_to_ms(20),
presignature_timeout: secs_to_ms(30),
signature_timeout: secs_to_ms(30),
}
#[serde(flatten)]
pub entries: HashMap<String, DynamicValue>,
}

impl Config {
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
let value = self.entries.get(key)?;
Some(&value.0)
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct DynamicValue(serde_json::Value);

impl From<serde_json::Value> for DynamicValue {
fn from(value: serde_json::Value) -> Self {
Self(value)
}
}

impl BorshSerialize for DynamicValue {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
let buf = serde_json::to_vec(&self.0)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
BorshSerialize::serialize(&buf, writer)
}
}

impl BorshDeserialize for DynamicValue {
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
let buf: Vec<u8> = BorshDeserialize::deserialize_reader(reader)?;
let value = serde_json::from_slice(&buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(Self(value))
}
}

Expand All @@ -28,3 +51,36 @@ pub const fn secs_to_ms(secs: u64) -> u64 {
pub const fn min_to_ms(min: u64) -> u64 {
min * 60 * 1000
}

#[cfg(test)]
mod tests {
use crate::config::Config;

#[test]
fn test_load_config() {
let config_str: serde_json::Value = serde_json::from_str(
r#"{
"triple_timeout": 20000,
"presignature_timeout": 30000,
"signature_timeout": 30000,
"string": "value",
"integer": 1000
}"#,
)
.unwrap();

let config_macro = serde_json::json!({
"triple_timeout": 20000,
"presignature_timeout": 30000,
"signature_timeout": 30000,
"string": "value",
"integer": 1000,
});

assert_eq!(config_str, config_macro);

let config: Config = serde_json::from_value(config_macro).unwrap();
assert_eq!(config.get("string").unwrap(), &serde_json::json!("value"),);
assert_eq!(config.get("integer").unwrap(), &serde_json::json!(1000),);
}
}
4 changes: 4 additions & 0 deletions chain-signatures/contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum PublicKeyError {
pub enum InitError {
#[error("Threshold cannot be greater than the number of candidates")]
ThresholdTooHigh,
#[error("Cannot load in contract due to missing state")]
ContractStateIsMissing,
}

#[derive(Debug, thiserror::Error)]
Expand All @@ -68,6 +70,8 @@ pub enum VoteError {
ParticipantsBelowThreshold,
#[error("Update not found.")]
UpdateNotFound,
#[error("Attached deposit is lower than required. Attached: {0}, Required: {1}.")]
InsufficientDeposit(u128, u128),
#[error("Unexpected protocol state: {0}")]
UnexpectedProtocolState(String),
#[error("Unexpected: {0}")]
Expand Down
97 changes: 67 additions & 30 deletions chain-signatures/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use k256::Scalar;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LookupMap;
use near_sdk::{
env, log, near_bindgen, AccountId, CryptoHash, Gas, GasWeight, NearToken, PromiseError,
PublicKey,
env, log, near_bindgen, AccountId, CryptoHash, Gas, GasWeight, NearToken, Promise,
PromiseError, PublicKey,
};
use primitives::{
CandidateInfo, Candidates, Participants, PkVotes, SignRequest, SignaturePromiseError,
Expand Down Expand Up @@ -109,7 +109,6 @@ impl VersionedMpcContract {
/// To avoid overloading the network with too many requests,
/// we ask for a small deposit for each signature request.
/// The fee changes based on how busy the network is.
#[allow(unused_variables)]
#[handle_result]
#[payable]
pub fn sign(&mut self, request: SignRequest) -> Result<near_sdk::Promise, MpcContractError> {
Expand Down Expand Up @@ -501,70 +500,77 @@ impl VersionedMpcContract {
Ok(true)
} else {
Err(MpcContractError::VoteError(
VoteError::UnexpectedProtocolState("Running".to_string()),
VoteError::UnexpectedProtocolState("Running: invalid epoch".to_string()),
))
}
}
_ => Err(MpcContractError::VoteError(
VoteError::UnexpectedProtocolState("Running".to_string()),
ProtocolContractState::NotInitialized => Err(MpcContractError::VoteError(
VoteError::UnexpectedProtocolState("NotInitialized".to_string()),
)),
ProtocolContractState::Initializing(_) => Err(MpcContractError::VoteError(
VoteError::UnexpectedProtocolState("Initializing".to_string()),
)),
}
}

/// Propose an update to the contract. [`Update`] are all the possible updates that can be proposed.
///
/// returns Some(id) if the proposal was successful, None otherwise
#[payable]
#[handle_result]
pub fn propose_update(
&mut self,
code: Option<Vec<u8>>,
config: Option<Config>,
) -> Result<UpdateId, VoteError> {
) -> Result<UpdateId, MpcContractError> {
// Only voters can propose updates:
self.voter()?;
let proposer = self.voter()?;

let attached = env::attached_deposit();
let required = ProposedUpdates::required_deposit(&code, &config);
if attached < required {
return Err(MpcContractError::from(VoteError::InsufficientDeposit(
attached.as_yoctonear(),
required.as_yoctonear(),
)));
}

let Some(id) = self.proposed_updates().propose(code, config) else {
return Err(VoteError::Unexpected(
return Err(MpcContractError::from(VoteError::Unexpected(
"cannot propose update due to incorrect parameters".into(),
));
)));
};

env::log_str(&format!("id={id:?}"));
// Refund the difference if the propser attached more than required.
if let Some(diff) = attached.checked_sub(required) {
if diff > NearToken::from_yoctonear(0) {
Promise::new(proposer).transfer(diff);
}
}

Ok(id)
}

/// Vote for a proposed update given the [`UpdateId`] of the update.
///
/// Returns Ok(true) if the amount of participants surpassed the threshold and the update was executed.
/// Returns Ok(true) if the amount of voters surpassed the threshold and the update was executed.
/// Returns Ok(false) if the amount of voters did not surpass the threshold. Returns Err if the update
/// was not found or if the voter is not a participant in the protocol.
#[handle_result]
pub fn vote_update(&mut self, id: UpdateId) -> Result<bool, MpcContractError> {
let threshold = match self {
Self::V0(contract) => match &contract.protocol_state {
ProtocolContractState::Running(state) => state.threshold,
ProtocolContractState::Resharing(state) => state.threshold,
_ => {
return Err(MpcContractError::VoteError(
VoteError::UnexpectedProtocolState("Initialized or NotInitialized".into()),
))
}
},
};

let threshold = self.threshold()?;
let voter = self.voter()?;
let Some(votes) = self.proposed_updates().vote(&id, voter) else {
return Err(MpcContractError::VoteError(VoteError::UpdateNotFound));
return Err(MpcContractError::from(VoteError::UpdateNotFound));
};

// Not enough votes, wait for more.
if votes.len() < threshold {
return Ok(false);
}

let Some(_promise) =
self.proposed_updates()
.do_update(&id, "update_config", UPDATE_CONFIG_GAS)
else {
return Err(MpcContractError::VoteError(VoteError::UpdateNotFound));
let Some(_promise) = self.proposed_updates().do_update(&id, UPDATE_CONFIG_GAS) else {
return Err(MpcContractError::from(VoteError::UpdateNotFound));
};

Ok(true)
Expand Down Expand Up @@ -634,6 +640,22 @@ impl VersionedMpcContract {
}))
}

/// This will be called internally by the contract to migrate the state when a new contract
/// is deployed. This function should be changed every time state is changed to do the proper
/// migrate flow.
///
/// If nothing is changed, then this function will just return the current state. If it fails
/// to read the state, then it will return an error.
#[private]
#[init(ignore_state)]
#[handle_result]
pub fn migrate() -> Result<Self, MpcContractError> {
let old: MpcContract = env::state_read().ok_or(MpcContractError::InitError(
InitError::ContractStateIsMissing,
))?;
Ok(VersionedMpcContract::V0(old))
}

pub fn state(&self) -> &ProtocolContractState {
match self {
Self::V0(mpc_contract) => &mpc_contract.protocol_state,
Expand Down Expand Up @@ -772,12 +794,27 @@ impl VersionedMpcContract {
}
}

fn threshold(&self) -> Result<usize, VoteError> {
match self {
Self::V0(contract) => match &contract.protocol_state {
ProtocolContractState::Initializing(state) => Ok(state.threshold),
ProtocolContractState::Running(state) => Ok(state.threshold),
ProtocolContractState::Resharing(state) => Ok(state.threshold),
ProtocolContractState::NotInitialized => {
Err(VoteError::UnexpectedProtocolState("NotInitialized".into()))
}
},
}
}

fn proposed_updates(&mut self) -> &mut ProposedUpdates {
match self {
Self::V0(contract) => &mut contract.proposed_updates,
}
}

/// Get our own account id as a voter. Check to see if we are a participant in the protocol.
/// If we are not a participant, return an error.
fn voter(&self) -> Result<AccountId, VoteError> {
let voter = env::signer_account_id();
match self {
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/contract/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::collections::HashSet;

use borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{AccountId, PublicKey};
use serde::{Deserialize, Serialize};

use crate::primitives::{Candidates, Participants, PkVotes, Votes};

Expand Down
Loading

0 comments on commit fa6fa9b

Please sign in to comment.