Skip to content

Commit

Permalink
chore: made ProposedUpdates use IterableMap underneath due contracts …
Browse files Browse the repository at this point in the history
…taking too much space (#737)

* Separate out large test files in contract/

* Added test for proposing multiple contracts

* Made ProposeUpdates entries use IterableMap instead

* Reordered contract/tests
  • Loading branch information
ChaoticTempest authored Jul 29, 2024
1 parent 46599a1 commit d368dd5
Show file tree
Hide file tree
Showing 6 changed files with 712 additions and 643 deletions.
1 change: 1 addition & 0 deletions chain-signatures/contract/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod hpke {
#[borsh(crate = "near_sdk::borsh")]
pub enum StorageKey {
PendingRequests,
ProposedUpdatesEntries,
}

/// The index into calling the YieldResume feature of NEAR. This will allow to resume
Expand Down
17 changes: 14 additions & 3 deletions chain-signatures/contract/src/update.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use std::hash::Hash;

use crate::config::Config;
use crate::primitives::StorageKey;

use borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::store::IterableMap;
use near_sdk::{env, AccountId, Gas, NearToken, Promise};

#[derive(
Expand Down Expand Up @@ -58,12 +60,21 @@ struct UpdateEntry {
bytes_used: u128,
}

#[derive(Default, Debug, BorshSerialize, BorshDeserialize)]
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct ProposedUpdates {
entries: HashMap<UpdateId, UpdateEntry>,
entries: IterableMap<UpdateId, UpdateEntry>,
id: UpdateId,
}

impl Default for ProposedUpdates {
fn default() -> Self {
Self {
entries: IterableMap::new(StorageKey::ProposedUpdatesEntries),
id: UpdateId::default(),
}
}
}

impl ProposedUpdates {
pub fn required_deposit(code: &Option<Vec<u8>>, config: &Option<Config>) -> NearToken {
required_deposit(bytes_used(code, config))
Expand Down
272 changes: 272 additions & 0 deletions chain-signatures/contract/tests/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;

use crypto_shared::kdf::{check_ec_signature, derive_secret_key};
use crypto_shared::{
derive_epsilon, derive_key, ScalarExt as _, SerializableAffinePoint, SerializableScalar,
SignatureResponse,
};
use digest::{Digest, FixedOutput};
use ecdsa::signature::Verifier;
use k256::elliptic_curve::ops::Reduce;
use k256::elliptic_curve::point::DecompressPoint as _;
use k256::elliptic_curve::sec1::ToEncodedPoint;
use k256::{AffinePoint, FieldBytes, Scalar, Secp256k1};
use mpc_contract::primitives::{
CandidateInfo, ParticipantInfo, Participants, SignRequest, SignatureRequest,
};
use mpc_contract::update::UpdateId;
use near_sdk::NearToken;
use near_workspaces::network::Sandbox;
use near_workspaces::types::AccountId;
use near_workspaces::{Account, Contract, Worker};
use signature::DigestSigner;

pub const CONTRACT_FILE_PATH: &str =
"../../target/wasm32-unknown-unknown/release/mpc_contract.wasm";
pub const INVALID_CONTRACT: &str = "../res/mpc_test_contract.wasm";
pub const PARTICIPANT_LEN: usize = 3;

pub fn candidates(names: Option<Vec<AccountId>>) -> HashMap<AccountId, CandidateInfo> {
let mut candidates: HashMap<AccountId, CandidateInfo> = HashMap::new();
let names = names.unwrap_or_else(|| {
vec![
"alice.near".parse().unwrap(),
"bob.near".parse().unwrap(),
"caesar.near".parse().unwrap(),
]
});

for account_id in names {
candidates.insert(
account_id.clone(),
CandidateInfo {
account_id,
url: "127.0.0.1".into(),
cipher_pk: [0; 32],
sign_pk: near_sdk::PublicKey::from_str(
"ed25519:J75xXmF7WUPS3xCm3hy2tgwLCKdYM1iJd4BWF8sWVnae",
)
.unwrap(),
},
);
}
candidates
}

/// Create `amount` accounts and return them along with the candidate info.
pub async fn accounts(
worker: &Worker<Sandbox>,
) -> (Vec<Account>, HashMap<AccountId, CandidateInfo>) {
let mut accounts = Vec::with_capacity(PARTICIPANT_LEN);
for _ in 0..PARTICIPANT_LEN {
let account = worker.dev_create_account().await.unwrap();
accounts.push(account);
}
let candidates = candidates(Some(accounts.iter().map(|a| a.id().clone()).collect()));
(accounts, candidates)
}

pub async fn init() -> (Worker<Sandbox>, Contract) {
let worker = near_workspaces::sandbox().await.unwrap();
let wasm = std::fs::read(CONTRACT_FILE_PATH).unwrap();
let contract = worker.dev_deploy(&wasm).await.unwrap();
(worker, contract)
}

pub async fn init_with_candidates(
pk: Option<near_crypto::PublicKey>,
) -> (Worker<Sandbox>, Contract, Vec<Account>) {
let (worker, contract) = init().await;
let (accounts, candidates) = accounts(&worker).await;

let init = if let Some(pk) = pk {
let participants_map = candidates
.into_iter()
.map(|(k, v)| (k, Into::<ParticipantInfo>::into(v)))
.collect::<BTreeMap<_, _>>();
let participants = Participants {
next_id: participants_map.len().try_into().unwrap(),
participants: participants_map.clone(),
account_to_participant_id: participants_map
.into_iter()
.enumerate()
.map(|(id, (account_id, _))| (account_id, id.try_into().unwrap()))
.collect(),
};
contract
.call("init_running")
.args_json(serde_json::json!({
"epoch": 0,
"threshold": 2,
"participants": participants,
"public_key": pk,
}))
.transact()
.await
.unwrap()
.into_result()
.unwrap()
} else {
contract
.call("init")
.args_json(serde_json::json!({
"threshold": 2,
"candidates": candidates
}))
.transact()
.await
.unwrap()
.into_result()
.unwrap()
};
dbg!(init);
(worker, contract, accounts)
}

pub async fn init_env() -> (Worker<Sandbox>, Contract, Vec<Account>, k256::SecretKey) {
let sk = k256::SecretKey::random(&mut rand::thread_rng());
let pk = sk.public_key();
let (worker, contract, accounts) =
init_with_candidates(Some(near_crypto::PublicKey::SECP256K1(
near_crypto::Secp256K1PublicKey::try_from(
&pk.as_affine().to_encoded_point(false).as_bytes()[1..65],
)
.unwrap(),
)))
.await;

(worker, contract, accounts, sk)
}

/// Process the message, creating the same hash with type of Digest, Scalar, and [u8; 32]
pub async fn process_message(msg: &str) -> (impl Digest, k256::Scalar, [u8; 32]) {
let msg = msg.as_bytes();
let digest = <k256::Secp256k1 as ecdsa::hazmat::DigestPrimitive>::Digest::new_with_prefix(msg);
let bytes: FieldBytes = digest.clone().finalize_fixed();
let scalar_hash =
<k256::Scalar as Reduce<<Secp256k1 as k256::elliptic_curve::Curve>::Uint>>::reduce_bytes(
&bytes,
);

let payload_hash: [u8; 32] = bytes.into();
(digest, scalar_hash, payload_hash)
}

pub async fn create_response(
predecessor_id: &AccountId,
msg: &str,
path: &str,
sk: &k256::SecretKey,
) -> ([u8; 32], SignatureRequest, SignatureResponse) {
let (digest, scalar_hash, payload_hash) = process_message(msg).await;
let pk = sk.public_key();

let epsilon = derive_epsilon(predecessor_id, path);
let derived_sk = derive_secret_key(sk, epsilon);
let derived_pk = derive_key(pk.into(), epsilon);
let signing_key = k256::ecdsa::SigningKey::from(&derived_sk);
let verifying_key =
k256::ecdsa::VerifyingKey::from(&k256::PublicKey::from_affine(derived_pk).unwrap());

let (signature, _): (ecdsa::Signature<Secp256k1>, _) =
signing_key.try_sign_digest(digest).unwrap();
verifying_key.verify(msg.as_bytes(), &signature).unwrap();

let s = signature.s();
let (r_bytes, _s_bytes) = signature.split_bytes();
let payload_hash_s = Scalar::from_bytes(payload_hash).unwrap();
let respond_req = SignatureRequest::new(payload_hash_s, predecessor_id, path);
let big_r =
AffinePoint::decompress(&r_bytes, k256::elliptic_curve::subtle::Choice::from(0)).unwrap();
let s: k256::Scalar = *s.as_ref();

let recovery_id = if check_ec_signature(&derived_pk, &big_r, &s, scalar_hash, 0).is_ok() {
0
} else if check_ec_signature(&derived_pk, &big_r, &s, scalar_hash, 1).is_ok() {
1
} else {
panic!("unable to use recovery id of 0 or 1");
};

let respond_resp = SignatureResponse {
big_r: SerializableAffinePoint {
affine_point: big_r,
},
s: SerializableScalar { scalar: s },
recovery_id,
};

(payload_hash, respond_req, respond_resp)
}

pub async fn sign_and_validate(
request: &SignRequest,
respond: Option<(&SignatureRequest, &SignatureResponse)>,
contract: &Contract,
) -> anyhow::Result<()> {
let status = contract
.call("sign")
.args_json(serde_json::json!({
"request": request,
}))
.deposit(NearToken::from_yoctonear(1))
.max_gas()
.transact_async()
.await?;
dbg!(&status);

tokio::time::sleep(std::time::Duration::from_secs(3)).await;

if let Some((respond_req, respond_resp)) = respond {
// Call `respond` as if we are the MPC network itself.
let respond = contract
.call("respond")
.args_json(serde_json::json!({
"request": respond_req,
"response": respond_resp
}))
.max_gas()
.transact()
.await?;
dbg!(&respond);
}

let execution = status.await?;
dbg!(&execution);
let execution = execution.into_result()?;

// Finally wait the result:
let returned_resp: SignatureResponse = execution.json()?;
if let Some((_, respond_resp)) = respond {
assert_eq!(
&returned_resp, respond_resp,
"Returned signature request does not match"
);
}

Ok(())
}

pub async fn vote_update_till_completion(
contract: &Contract,
accounts: &[Account],
proposal_id: &UpdateId,
) {
for voter in accounts {
let execution = voter
.call(contract.id(), "vote_update")
.args_json(serde_json::json!({
"id": proposal_id,
}))
.max_gas()
.transact()
.await
.unwrap();

// Met the threshold, voting completed.
if execution.is_failure() {
break;
}
}
}
Loading

0 comments on commit d368dd5

Please sign in to comment.