diff --git a/API.md b/API.md index b9c36097..c505ab5e 100644 --- a/API.md +++ b/API.md @@ -13,9 +13,9 @@ pub struct SignRequest { pub key_version: u32, } -pub struct SignatureResponse { - pub big_r: SerializableAffinePoint, - pub s: SerializableScalar, +pub struct Signature { + pub big_r: AffinePoint, + pub s: Scalar, pub recovery_id: u8, } ``` @@ -57,7 +57,7 @@ For more details check `User contract API` impl block in the [chain-signatures/c 1. Mainnet: `v1.signer` 2. Testnet: `v1.sigenr-prod.testnet` -# Interact using NEAR CLI +# Interact using NEAR CLI There is an `Example Commands` in the [chain-signatures/contract/example.md](./chain-signature/contract/EXAMPLE.md) file. diff --git a/Cargo.lock b/Cargo.lock index a9c92304..6b3ea2d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3278,6 +3278,7 @@ dependencies = [ "mpc-crypto", "mpc-keys", "mpc-node", + "mpc-primitives", "near-account-id", "near-crypto 0.26.0", "near-fetch", @@ -3653,6 +3654,7 @@ dependencies = [ "ecdsa 0.16.9", "k256", "mpc-crypto", + "mpc-primitives", "near-crypto 0.27.0", "near-sdk", "near-workspaces 0.15.0", @@ -3669,13 +3671,10 @@ name = "mpc-crypto" version = "1.0.0" dependencies = [ "anyhow", - "borsh", "getrandom", "k256", "near-account-id", "near-sdk", - "serde", - "serde_json", "sha3", "web3", ] @@ -3720,6 +3719,7 @@ dependencies = [ "mpc-contract", "mpc-crypto", "mpc-keys", + "mpc-primitives", "near-account-id", "near-crypto 0.26.0", "near-fetch", @@ -3747,6 +3747,24 @@ dependencies = [ "web3", ] +[[package]] +name = "mpc-primitives" +version = "1.0.0" +dependencies = [ + "borsh", + "ciborium", + "getrandom", + "hex", + "k256", + "mpc-crypto", + "near-account-id", + "near-sdk", + "serde", + "serde_bytes", + "serde_json", + "sha3", +] + [[package]] name = "native-tls" version = "0.2.12" diff --git a/Cargo.toml b/Cargo.toml index b50823ce..7aa3dbca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "chain-signatures/contract", "chain-signatures/keys", "chain-signatures/node", + "chain-signatures/primitives", "integration-tests", ] @@ -16,6 +17,7 @@ version = "1.0.0" anyhow = { version = "1.0.95", features = ["backtrace"] } borsh = "1.5.3" cait-sith = { git = "https://github.com/sig-net/cait-sith", rev = "9f34e8c", features = ["k256"] } +ciborium = "0.2.2" clap = { version = "4.5.4", features = ["derive", "env"] } deadpool-redis = "0.18.0" hex = "0.4.3" @@ -30,6 +32,7 @@ k256 = { version = "0.13.1", features = [ rand = "0.8.5" reqwest = { version = "0.11.16", features = ["blocking", "json"] } serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11.15" serde_json = "1" sha3 = "0.10.8" thiserror = "1" @@ -40,12 +43,14 @@ url = { version = "2.4.0", features = ["serde"] } web3 = "0.19.0" near-account-id = "1.0.0" +near-primitives = "0.26.0" near-sdk = { version = "5.6.0", features = ["legacy", "unit-testing", "unstable"] } mpc-contract = { path = "./chain-signatures/contract" } mpc-crypto = { path = "./chain-signatures/crypto" } mpc-keys = { path = "./chain-signatures/keys" } mpc-node = { path = "./chain-signatures/node" } +mpc-primitives = { path = "./chain-signatures/primitives" } [patch.crates-io] # TODO: trigger Cargo.lock update for x25519-dalek once they release. diff --git a/chain-signatures/contract/Cargo.toml b/chain-signatures/contract/Cargo.toml index cff883c4..3f6dc757 100644 --- a/chain-signatures/contract/Cargo.toml +++ b/chain-signatures/contract/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib", "lib"] borsh.workspace = true k256.workspace = true mpc-crypto.workspace = true +mpc-primitives.workspace = true near-sdk.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/chain-signatures/contract/src/lib.rs b/chain-signatures/contract/src/lib.rs index a37c797a..e2522fe3 100644 --- a/chain-signatures/contract/src/lib.rs +++ b/chain-signatures/contract/src/lib.rs @@ -12,8 +12,9 @@ use k256::elliptic_curve::sec1::ToEncodedPoint; use k256::Scalar; use mpc_crypto::{ derive_epsilon_near, derive_key, kdf::check_ec_signature, near_public_key_to_affine_point, - types::SignatureResponse, ScalarExt as _, + ScalarExt as _, }; +use mpc_primitives::{SignId, Signature}; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::LookupMap; use near_sdk::json_types::U128; @@ -22,8 +23,8 @@ use near_sdk::{ PromiseError, PublicKey, }; use primitives::{ - CandidateInfo, Candidates, ContractSignatureRequest, Participants, PkVotes, SignRequest, - SignaturePromiseError, SignatureRequest, SignatureResult, StorageKey, Votes, YieldIndex, + CandidateInfo, Candidates, InternalSignRequest, Participants, PendingRequest, PkVotes, + SignRequest, SignaturePromiseError, SignatureResult, StorageKey, Votes, YieldIndex, }; use std::collections::{BTreeMap, HashSet}; @@ -67,31 +68,39 @@ impl Default for VersionedMpcContract { #[derive(BorshDeserialize, BorshSerialize, Debug)] pub struct MpcContract { protocol_state: ProtocolContractState, - pending_requests: LookupMap>, + pending_requests: LookupMap, request_counter: u32, proposed_updates: ProposedUpdates, config: Config, } impl MpcContract { - fn mark_request_received(&mut self, request: &SignatureRequest) { - if self.pending_requests.insert(request, &None).is_none() { - self.request_counter += 1; - } - } - - fn add_request(&mut self, request: &SignatureRequest, data_id: CryptoHash) { + fn lock_request(&mut self, sign_id: &SignId, payload: Scalar, epsilon: Scalar) { if self .pending_requests - .insert(request, &Some(YieldIndex { data_id })) + .insert( + sign_id, + &PendingRequest { + payload, + epsilon, + index: None, + }, + ) .is_none() { self.request_counter += 1; } } - fn remove_request(&mut self, request: SignatureRequest) -> Result<(), Error> { - if self.pending_requests.remove(&request).is_some() { + fn add_request(&mut self, sign_id: &SignId, data_id: CryptoHash) { + if let Some(mut request) = self.pending_requests.get(sign_id) { + request.index = Some(YieldIndex { data_id }); + self.pending_requests.insert(sign_id, &request); + } + } + + fn remove_request(&mut self, sign_id: &SignId) -> Result<(), Error> { + if self.pending_requests.remove(sign_id).is_some() { self.request_counter -= 1; Ok(()) } else { @@ -129,13 +138,13 @@ impl VersionedMpcContract { #[payable] pub fn sign(&mut self, request: SignRequest) -> Result { let SignRequest { - payload, + payload: payload_bytes, path, key_version, } = request; // It's important we fail here because the MPC nodes will fail in an identical way. // This allows users to get the error message - let payload = Scalar::from_bytes(payload).ok_or( + let payload = Scalar::from_bytes(payload_bytes).ok_or( InvalidParameters::MalformedPayload .message("Payload hash cannot be convereted to Scalar"), )?; @@ -169,20 +178,23 @@ impl VersionedMpcContract { } } let predecessor = env::predecessor_account_id(); - let request = SignatureRequest::new(payload, &predecessor, &path); - if !self.request_already_exists(&request) { + let sign_id = SignId::from_parts(&predecessor, &payload_bytes, &path, key_version); + if !self.contains_request(&sign_id) { log!( "sign: predecessor={predecessor}, payload={payload:?}, path={path:?}, key_version={key_version}", ); - env::log_str(&serde_json::to_string(&near_sdk::env::random_seed_array()).unwrap()); - self.mark_request_received(&request); - let contract_signature_request = ContractSignatureRequest { - request, + let entropy = near_sdk::env::random_seed_array(); + env::log_str(&serde_json::to_string(&entropy).unwrap()); + let epsilon = derive_epsilon_near(&predecessor, &path); + + self.lock_request(&sign_id, payload, epsilon); + let request = InternalSignRequest { + id: sign_id, requester: predecessor, deposit, required_deposit: NearToken::from_yoctonear(required_deposit), }; - Ok(Self::ext(env::current_account_id()).sign_helper(contract_signature_request)) + Ok(Self::ext(env::current_account_id()).sign_helper(request)) } else { Err(SignError::RequestCollision.into()) } @@ -245,59 +257,49 @@ impl VersionedMpcContract { #[near_bindgen] impl VersionedMpcContract { #[handle_result] - pub fn respond( - &mut self, - request: SignatureRequest, - response: SignatureResponse, - ) -> Result<(), Error> { + pub fn respond(&mut self, sign_id: SignId, signature: Signature) -> Result<(), Error> { let protocol_state = self.mutable_state(); + if !matches!(protocol_state, ProtocolContractState::Running(_)) { + return Err(InvalidState::ProtocolStateNotRunning.into()); + } - if let ProtocolContractState::Running(_) = protocol_state { - let signer = env::signer_account_id(); - log!( - "respond: signer={}, request={:?} big_r={:?} s={:?}", - &signer, - &request, - &response.big_r, - &response.s - ); + let signer = env::signer_account_id(); + log!( + "respond: signer={}, sign_id={:?} big_r={:?} s={:?}", + &signer, + &sign_id, + &signature.big_r, + &signature.s + ); - // generate the expected public key - let pk = self.public_key()?; - let expected_public_key = - derive_key(near_public_key_to_affine_point(pk), request.epsilon.scalar); - - // Check the signature is correct - if check_ec_signature( - &expected_public_key, - &response.big_r.affine_point, - &response.s.scalar, - request.payload_hash.scalar, - response.recovery_id, - ) - .is_err() - { - return Err(RespondError::InvalidSignature.into()); - } + let Some(PendingRequest { + payload, + epsilon, + index: Some(index), + }) = self.get_request(&sign_id) + else { + return Err(InvalidParameters::RequestNotFound.into()); + }; - match self { - Self::V0(mpc_contract) => { - if let Some(Some(YieldIndex { data_id })) = - mpc_contract.pending_requests.get(&request) - { - env::promise_yield_resume( - &data_id, - &serde_json::to_vec(&response).unwrap(), - ); - Ok(()) - } else { - Err(InvalidParameters::RequestNotFound.into()) - } - } - } - } else { - Err(InvalidState::ProtocolStateNotRunning.into()) + // generate the expected public key + let pk = self.public_key()?; + let expected_public_key = derive_key(near_public_key_to_affine_point(pk), epsilon); + + // Check the signature is correct + if check_ec_signature( + &expected_public_key, + &signature.big_r, + &signature.s, + payload, + signature.recovery_id, + ) + .is_err() + { + return Err(RespondError::InvalidSignature.into()); } + + env::promise_yield_resume(&index.data_id, &serde_json::to_vec(&signature).unwrap()); + Ok(()) } #[handle_result] @@ -696,12 +698,12 @@ impl VersionedMpcContract { } #[private] - pub fn sign_helper(&mut self, contract_signature_request: ContractSignatureRequest) { + pub fn sign_helper(&mut self, request: InternalSignRequest) { match self { Self::V0(mpc_contract) => { let yield_promise = env::promise_yield_create( "clear_state_on_finish", - &serde_json::to_vec(&(&contract_signature_request,)).unwrap(), + &serde_json::to_vec(&(&request,)).unwrap(), CLEAR_STATE_ON_FINISH_CALL_GAS, GasWeight(0), DATA_ID_REGISTER, @@ -713,7 +715,7 @@ impl VersionedMpcContract { .try_into() .expect("conversion to CryptoHash failed"); - mpc_contract.add_request(&contract_signature_request.request, data_id); + mpc_contract.add_request(&request.id, data_id); // NOTE: there's another promise after the clear_state_on_finish to avoid any errors // that would rollback the state. @@ -736,8 +738,8 @@ impl VersionedMpcContract { #[handle_result] pub fn return_signature_on_finish( &mut self, - #[callback_unwrap] signature: SignatureResult, - ) -> Result { + #[callback_unwrap] signature: SignatureResult, + ) -> Result { match self { Self::V0(_) => match signature { SignatureResult::Ok(signature) => { @@ -749,14 +751,14 @@ impl VersionedMpcContract { } } - fn refund_on_fail(request: &ContractSignatureRequest) { + fn refund_on_fail(request: &InternalSignRequest) { let amount = request.deposit; let to = request.requester.clone(); log!("refund {amount} to {to} due to fail"); Promise::new(to).transfer(amount); } - fn refund_on_success(request: &ContractSignatureRequest) { + fn refund_on_success(request: &InternalSignRequest) { let deposit = request.deposit; let required = request.required_deposit; if let Some(diff) = deposit.checked_sub(required) { @@ -772,29 +774,28 @@ impl VersionedMpcContract { #[handle_result] pub fn clear_state_on_finish( &mut self, - contract_signature_request: ContractSignatureRequest, - #[callback_result] signature: Result, - ) -> Result, Error> { + request: InternalSignRequest, + #[callback_result] signature: Result, + ) -> Result, Error> { match self { Self::V0(mpc_contract) => { // Clean up the local state - let result = - mpc_contract.remove_request(contract_signature_request.request.clone()); + let result = mpc_contract.remove_request(&request.id); if result.is_err() { // refund must happen in clear_state_on_finish, because regardless of this success or fail // the promise created by clear_state_on_finish is executed, because of callback_unwrap and // promise_then. but if return_signature_on_finish fail (returns error), the promise created // by it won't execute. - Self::refund_on_fail(&contract_signature_request); + Self::refund_on_fail(&request); result?; } match signature { Ok(signature) => { - Self::refund_on_success(&contract_signature_request); + Self::refund_on_success(&request); Ok(SignatureResult::Ok(signature)) } Err(_) => { - Self::refund_on_fail(&contract_signature_request); + Self::refund_on_fail(&request); Ok(SignatureResult::Err(SignaturePromiseError::Failed)) } } @@ -817,15 +818,21 @@ impl VersionedMpcContract { } } - fn request_already_exists(&self, request: &SignatureRequest) -> bool { + fn contains_request(&self, id: &SignId) -> bool { + match self { + Self::V0(mpc_contract) => mpc_contract.pending_requests.contains_key(id), + } + } + + fn lock_request(&mut self, id: &SignId, payload: Scalar, epsilon: Scalar) { match self { - Self::V0(mpc_contract) => mpc_contract.pending_requests.contains_key(request), + Self::V0(ref mut mpc_contract) => mpc_contract.lock_request(id, payload, epsilon), } } - fn mark_request_received(&mut self, request: &SignatureRequest) { + fn get_request(&self, id: &SignId) -> Option { match self { - Self::V0(ref mut mpc_contract) => mpc_contract.mark_request_received(request), + Self::V0(mpc_contract) => mpc_contract.pending_requests.get(id), } } diff --git a/chain-signatures/contract/src/primitives.rs b/chain-signatures/contract/src/primitives.rs index a16f569e..ed22974b 100644 --- a/chain-signatures/contract/src/primitives.rs +++ b/chain-signatures/contract/src/primitives.rs @@ -1,5 +1,4 @@ -use k256::Scalar; -use mpc_crypto::{derive_epsilon_near, SerializableScalar}; +use mpc_primitives::{bytes::borsh_scalar, SignId}; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{AccountId, BorshStorageKey, CryptoHash, NearToken, PublicKey}; @@ -24,34 +23,20 @@ pub struct YieldIndex { pub data_id: CryptoHash, } -#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)] -#[borsh(crate = "near_sdk::borsh")] -pub struct SignatureRequest { - pub epsilon: SerializableScalar, - pub payload_hash: SerializableScalar, -} - -#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[borsh(crate = "near_sdk::borsh")] -pub struct ContractSignatureRequest { - pub request: SignatureRequest, - pub requester: AccountId, - pub deposit: NearToken, - pub required_deposit: NearToken, -} - -impl SignatureRequest { - pub fn new(payload_hash: Scalar, predecessor_id: &AccountId, path: &str) -> Self { - let epsilon = derive_epsilon_near(predecessor_id, path); - let epsilon = SerializableScalar { scalar: epsilon }; - let payload_hash = SerializableScalar { - scalar: payload_hash, - }; - SignatureRequest { - epsilon, - payload_hash, - } - } +pub struct PendingRequest { + pub index: Option, + #[borsh( + serialize_with = "borsh_scalar::serialize", + deserialize_with = "borsh_scalar::deserialize_reader" + )] + pub payload: k256::Scalar, + #[borsh( + serialize_with = "borsh_scalar::serialize", + deserialize_with = "borsh_scalar::deserialize_reader" + )] + pub epsilon: k256::Scalar, } #[derive( @@ -326,6 +311,15 @@ impl PkVotes { } } +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone)] +#[borsh(crate = "near_sdk::borsh")] +pub struct InternalSignRequest { + pub id: SignId, + pub requester: AccountId, + pub deposit: NearToken, + pub required_deposit: NearToken, +} + #[derive(Serialize, Deserialize, BorshDeserialize, BorshSerialize, Debug)] pub struct SignRequest { pub payload: [u8; 32], diff --git a/chain-signatures/contract/tests/common.rs b/chain-signatures/contract/tests/common.rs index 58165850..3082a98e 100644 --- a/chain-signatures/contract/tests/common.rs +++ b/chain-signatures/contract/tests/common.rs @@ -6,16 +6,12 @@ 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 k256::{AffinePoint, FieldBytes, Secp256k1}; +use mpc_contract::primitives::{CandidateInfo, ParticipantInfo, Participants, SignRequest}; use mpc_contract::update::UpdateId; use mpc_crypto::kdf::{check_ec_signature, derive_secret_key}; -use mpc_crypto::{ - derive_epsilon_near, derive_key, ScalarExt as _, SerializableAffinePoint, SerializableScalar, - SignatureResponse, -}; +use mpc_crypto::{derive_epsilon_near, derive_key}; +use mpc_primitives::{SignId, Signature}; use near_workspaces::network::Sandbox; use near_workspaces::types::{AccountId, NearToken}; use near_workspaces::{Account, Contract, Worker}; @@ -157,7 +153,7 @@ pub async fn create_response( msg: &str, path: &str, sk: &k256::SecretKey, -) -> ([u8; 32], SignatureRequest, SignatureResponse) { +) -> ([u8; 32], SignId, Signature) { let (digest, scalar_hash, payload_hash) = process_message(msg).await; let pk = sk.public_key(); @@ -174,8 +170,7 @@ pub async fn create_response( 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 sign_id = SignId::from_parts(predecessor_id, &payload_hash, path, 0); let big_r = AffinePoint::decompress(&r_bytes, k256::elliptic_curve::subtle::Choice::from(0)).unwrap(); let s: k256::Scalar = *s.as_ref(); @@ -188,20 +183,18 @@ pub async fn create_response( 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 }, + let respond_resp = Signature { + big_r, + s, recovery_id, }; - (payload_hash, respond_req, respond_resp) + (payload_hash, sign_id, respond_resp) } pub async fn sign_and_validate( request: &SignRequest, - respond: Option<(&SignatureRequest, &SignatureResponse)>, + respond: Option<(&SignId, &Signature)>, contract: &Contract, ) -> anyhow::Result<()> { let status = contract @@ -217,13 +210,13 @@ pub async fn sign_and_validate( tokio::time::sleep(std::time::Duration::from_secs(3)).await; - if let Some((respond_req, respond_resp)) = respond { + if let Some((sign_id, 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 + "sign_id": sign_id, + "signature": respond_resp })) .max_gas() .transact() @@ -236,7 +229,7 @@ pub async fn sign_and_validate( let execution = execution.into_result()?; // Finally wait the result: - let returned_resp: SignatureResponse = execution.json()?; + let returned_resp: Signature = execution.json()?; if let Some((_, respond_resp)) = respond { assert_eq!( &returned_resp, respond_resp, diff --git a/chain-signatures/contract/tests/sign.rs b/chain-signatures/contract/tests/sign.rs index a0ff793e..ff21431f 100644 --- a/chain-signatures/contract/tests/sign.rs +++ b/chain-signatures/contract/tests/sign.rs @@ -3,9 +3,9 @@ use common::{candidates, create_response, init, init_env, sign_and_validate}; use mpc_contract::errors; use mpc_contract::primitives::{CandidateInfo, SignRequest}; +use mpc_primitives::Signature; use near_workspaces::types::{AccountId, NearToken}; -use mpc_crypto::SignatureResponse; use std::collections::HashMap; #[tokio::test] @@ -68,8 +68,7 @@ async fn test_contract_sign_success_refund() -> anyhow::Result<()> { let msg = "hello world!"; println!("submitting: {msg}"); - let (payload_hash, respond_req, respond_resp) = - create_response(alice.id(), msg, path, &sk).await; + let (payload_hash, sign_id, respond_resp) = create_response(alice.id(), msg, path, &sk).await; let request = SignRequest { payload: payload_hash, path: path.into(), @@ -92,8 +91,8 @@ async fn test_contract_sign_success_refund() -> anyhow::Result<()> { let respond = contract .call("respond") .args_json(serde_json::json!({ - "request": respond_req, - "response": respond_resp + "sign_id": sign_id, + "signature": respond_resp })) .max_gas() .transact() @@ -106,7 +105,7 @@ async fn test_contract_sign_success_refund() -> anyhow::Result<()> { let execution = execution.into_result()?; // Finally wait the result: - let returned_resp: SignatureResponse = execution.json()?; + let returned_resp: Signature = execution.json()?; assert_eq!( returned_resp, respond_resp, "Returned signature request does not match" @@ -202,8 +201,7 @@ async fn test_contract_sign_request_deposits() -> anyhow::Result<()> { // Try to sign with no deposit, should fail. let msg = "without-deposit"; - let (payload_hash, respond_req, respond_resp) = - create_response(predecessor_id, msg, path, &sk).await; + let (payload_hash, sign_id, signature) = create_response(predecessor_id, msg, path, &sk).await; let request = SignRequest { payload: payload_hash, path: path.into(), @@ -225,8 +223,8 @@ async fn test_contract_sign_request_deposits() -> anyhow::Result<()> { let respond = contract .call("respond") .args_json(serde_json::json!({ - "request": respond_req, - "response": respond_resp + "sign_id": sign_id, + "signature": signature })) .max_gas() .transact() diff --git a/chain-signatures/crypto/Cargo.toml b/chain-signatures/crypto/Cargo.toml index d2db1583..74e24e5f 100644 --- a/chain-signatures/crypto/Cargo.toml +++ b/chain-signatures/crypto/Cargo.toml @@ -6,10 +6,7 @@ edition = "2021" [dependencies] k256.workspace = true anyhow.workspace = true -serde.workspace = true -borsh.workspace = true near-account-id.workspace = true -serde_json.workspace = true near-sdk.workspace = true sha3.workspace = true diff --git a/chain-signatures/crypto/src/lib.rs b/chain-signatures/crypto/src/lib.rs index 536dfa62..bbdc609b 100644 --- a/chain-signatures/crypto/src/lib.rs +++ b/chain-signatures/crypto/src/lib.rs @@ -4,9 +4,7 @@ pub mod types; use k256::elliptic_curve::sec1::FromEncodedPoint; use k256::EncodedPoint; pub use kdf::{derive_epsilon_near, derive_key, x_coordinate}; -pub use types::{ - PublicKey, ScalarExt, SerializableAffinePoint, SerializableScalar, SignatureResponse, -}; +pub use types::{PublicKey, ScalarExt}; // Our wasm runtime doesn't support good syncronous entropy. // We could use something VRF + pseudorandom here, but someone would likely shoot themselves in the foot with it. diff --git a/chain-signatures/crypto/src/types.rs b/chain-signatures/crypto/src/types.rs index 2f83fa8f..36e13610 100644 --- a/chain-signatures/crypto/src/types.rs +++ b/chain-signatures/crypto/src/types.rs @@ -1,9 +1,7 @@ -use borsh::{BorshDeserialize, BorshSerialize}; use k256::{ elliptic_curve::{bigint::ArrayEncoding, CurveArithmetic, PrimeField}, - AffinePoint, Scalar, Secp256k1, U256, + Scalar, Secp256k1, U256, }; -use serde::{Deserialize, Serialize}; pub type PublicKey = ::AffinePoint; @@ -41,101 +39,3 @@ fn scalar_fails_as_expected() { not_too_high[15] = 0xFD; assert!(Scalar::from_bytes(not_too_high).is_some()); } - -// Is there a better way to force a borsh serialization? -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] -pub struct SerializableScalar { - pub scalar: Scalar, -} - -impl From for SerializableScalar { - fn from(scalar: Scalar) -> Self { - SerializableScalar { scalar } - } -} - -impl BorshSerialize for SerializableScalar { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - let to_ser: [u8; 32] = self.scalar.to_bytes().into(); - BorshSerialize::serialize(&to_ser, writer) - } -} - -impl BorshDeserialize for SerializableScalar { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let from_ser: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?; - let scalar = Scalar::from_bytes(from_ser).ok_or(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Scalar bytes are not in the k256 field", - ))?; - Ok(SerializableScalar { scalar }) - } -} - -// Is there a better way to force a borsh serialization? -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] -pub struct SerializableAffinePoint { - pub affine_point: AffinePoint, -} - -impl BorshSerialize for SerializableAffinePoint { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - let to_ser: Vec = serde_json::to_vec(&self.affine_point)?; - BorshSerialize::serialize(&to_ser, writer) - } -} - -impl BorshDeserialize for SerializableAffinePoint { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let from_ser: Vec = BorshDeserialize::deserialize_reader(reader)?; - let affine_point = serde_json::from_slice(&from_ser)?; - Ok(SerializableAffinePoint { affine_point }) - } -} - -#[test] -fn serializeable_scalar_roundtrip() { - use k256::elliptic_curve::PrimeField; - let test_vec = vec![ - Scalar::ZERO, - Scalar::ONE, - Scalar::from_u128(u128::MAX), - Scalar::from_bytes([3; 32]).unwrap(), - ]; - - for scalar in test_vec.into_iter() { - let input = SerializableScalar { scalar }; - // Test borsh - { - let serialized = borsh::to_vec(&input).unwrap(); - let output: SerializableScalar = borsh::from_slice(&serialized).unwrap(); - assert_eq!(input, output, "Failed on {:?}", scalar); - } - - // Test Serde via JSON - { - let serialized = serde_json::to_vec(&input).unwrap(); - let output: SerializableScalar = serde_json::from_slice(&serialized).unwrap(); - assert_eq!(input, output, "Failed on {:?}", scalar); - } - } -} - -#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct SignatureResponse { - pub big_r: SerializableAffinePoint, - pub s: SerializableScalar, - pub recovery_id: u8, -} - -impl SignatureResponse { - pub fn new(big_r: AffinePoint, s: Scalar, recovery_id: u8) -> Self { - SignatureResponse { - big_r: SerializableAffinePoint { - affine_point: big_r, - }, - s: SerializableScalar { scalar: s }, - recovery_id, - } - } -} diff --git a/chain-signatures/node/Cargo.toml b/chain-signatures/node/Cargo.toml index 868c070c..91070d32 100644 --- a/chain-signatures/node/Cargo.toml +++ b/chain-signatures/node/Cargo.toml @@ -13,7 +13,6 @@ aws-config = "1.4" aws-sdk-s3 = "1.29" axum = "0.6.19" axum-extra = "0.7" -ciborium = "0.2.2" chrono = "0.4.24" google-datastore1 = "=5.0.4" google-secretmanager1 = "5" @@ -25,7 +24,6 @@ local-ip-address = "0.5.4" prometheus = "0.13.3" redis = "0.27.2" semver = "1.0.23" -serde_bytes = "0.11.15" sysinfo = "0.32.0" tokio-retry = "0.3" tracing-stackdriver = "0.10.0" @@ -34,6 +32,7 @@ tracing-stackdriver = "0.10.0" anyhow.workspace = true borsh.workspace = true cait-sith.workspace = true +ciborium.workspace = true clap.workspace = true deadpool-redis.workspace = true hex.workspace = true @@ -42,6 +41,7 @@ k256.workspace = true rand.workspace = true reqwest.workspace = true serde.workspace = true +serde_bytes.workspace = true serde_json.workspace = true sha3.workspace = true thiserror.workspace = true @@ -56,9 +56,10 @@ near-crypto = "0.26.0" near-fetch = "0.6.0" near-lake-framework = { git = "https://github.com/near/near-lake-framework-rs", branch = "node/2.3.0" } near-lake-primitives = { git = "https://github.com/near/near-lake-framework-rs", branch = "node/2.3.0" } -near-primitives = "0.26.0" +near-primitives.workspace = true near-sdk.workspace = true mpc-contract.workspace = true -mpc-keys.workspace = true mpc-crypto.workspace = true +mpc-keys.workspace = true +mpc-primitives.workspace = true diff --git a/chain-signatures/node/src/indexer.rs b/chain-signatures/node/src/indexer.rs index d5ae3793..8ac8193f 100644 --- a/chain-signatures/node/src/indexer.rs +++ b/chain-signatures/node/src/indexer.rs @@ -1,8 +1,9 @@ -use crate::protocol::Chain::NEAR; -use crate::protocol::{Chain, SignRequest}; +use crate::protocol::Chain; +use crate::protocol::IndexedSignRequest; use crate::storage::app_data_storage::AppDataStorage; use k256::Scalar; -use mpc_crypto::{derive_epsilon_near, ScalarExt}; +use mpc_crypto::{derive_epsilon_near, ScalarExt as _}; +use mpc_primitives::{SignArgs, SignId}; use near_account_id::AccountId; use near_lake_framework::{Lake, LakeBuilder, LakeContext}; use near_lake_primitives::actions::ActionMetaDataExt; @@ -79,16 +80,6 @@ struct UnvalidatedContractSignRequest { pub key_version: u32, } -/// A validated version of the sign request -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct ContractSignRequest { - #[serde(with = "crate::protocol::message::cbor_scalar")] - pub payload: Scalar, - pub path: String, - pub key_version: u32, - pub chain: Chain, -} - #[derive(Clone)] pub struct Indexer { app_data_storage: AppDataStorage, @@ -172,7 +163,7 @@ impl Indexer { struct Context { mpc_contract_id: AccountId, node_account_id: AccountId, - sign_tx: mpsc::Sender, + sign_tx: mpsc::Sender, indexer: Indexer, } @@ -193,7 +184,7 @@ async fn handle_block( tracing::warn!("{err}"); anyhow::bail!(err); }; - let ExecutionStatus::SuccessReceiptId(receipt_id) = receipt.status() else { + let ExecutionStatus::SuccessReceiptId(_receipt_id) = receipt.status() else { continue; }; let Some(function_call) = action.as_function_call() else { @@ -233,30 +224,35 @@ async fn handle_block( ); continue; }; - let epsilon = - derive_epsilon_near(&action.predecessor_id(), &arguments.request.path); + let predecessor_id = action.predecessor_id(); + let epsilon = derive_epsilon_near(&predecessor_id, &arguments.request.path); + let sign_id = SignId::from_parts( + &predecessor_id, + &arguments.request.payload, + &arguments.request.path, + arguments.request.key_version, + ); tracing::info!( - receipt_id = %receipt_id, - caller_id = receipt.predecessor_id().to_string(), - our_account = ctx.node_account_id.to_string(), + ?sign_id, + caller_id = ?predecessor_id, + our_account = ?ctx.node_account_id, payload = hex::encode(arguments.request.payload), key_version = arguments.request.key_version, entropy = hex::encode(entropy), "indexed new `sign` function call" ); - let request = ContractSignRequest { - payload, - path: arguments.request.path, - key_version: arguments.request.key_version, - chain: NEAR, - }; - pending_requests.push(SignRequest { - request_id: receipt_id.0, - request, - epsilon, - entropy, + pending_requests.push(IndexedSignRequest { + id: sign_id, + args: SignArgs { + entropy, + epsilon, + payload, + path: arguments.request.path, + key_version: arguments.request.key_version, + }, + chain: Chain::NEAR, // TODO: use indexer timestamp instead. - time_added: Instant::now(), + timestamp: Instant::now(), }); } } @@ -274,9 +270,11 @@ async fn handle_block( // This way we can revisit the same block if we failed while not having added the requests partially. for request in pending_requests { tracing::info!( - request_id = ?near_primitives::hash::CryptoHash(request.request_id), - payload = hex::encode(request.request.payload.to_bytes()), - entropy = hex::encode(request.entropy), + sign_id = ?request.id, + payload = hex::encode(request.args.payload.to_bytes()), + entropy = hex::encode(request.args.entropy), + epsilon = hex::encode(request.args.epsilon.to_bytes()), + "new sign request" ); if let Err(err) = ctx.sign_tx.send(request).await { @@ -303,7 +301,7 @@ pub fn run( options: &Options, mpc_contract_id: &AccountId, node_account_id: &AccountId, - sign_tx: mpsc::Sender, + sign_tx: mpsc::Sender, app_data_storage: AppDataStorage, rpc_client: near_fetch::Client, ) -> anyhow::Result<(JoinHandle>, Indexer)> { diff --git a/chain-signatures/node/src/indexer_eth.rs b/chain-signatures/node/src/indexer_eth.rs index 813b5768..7a41c1e0 100644 --- a/chain-signatures/node/src/indexer_eth.rs +++ b/chain-signatures/node/src/indexer_eth.rs @@ -1,10 +1,9 @@ -use crate::indexer::ContractSignRequest; -use crate::protocol::Chain::Ethereum; -use crate::protocol::SignRequest; +use crate::protocol::{Chain, IndexedSignRequest}; use hex::ToHex; use k256::Scalar; use mpc_crypto::kdf::derive_epsilon_eth; -use mpc_crypto::ScalarExt; +use mpc_crypto::ScalarExt as _; +use mpc_primitives::{SignArgs, SignId}; use near_account_id::AccountId; use serde::{Deserialize, Serialize}; use sha3::{Digest, Keccak256}; @@ -103,7 +102,7 @@ pub struct EthSignRequest { pub key_version: u32, } -fn sign_request_from_filtered_log(log: web3::types::Log) -> anyhow::Result { +fn sign_request_from_filtered_log(log: web3::types::Log) -> anyhow::Result { let event = parse_event(&log)?; tracing::debug!("found eth event: {:?}", event); if event.deposit == U256::from_dec_str("0").unwrap() { @@ -132,16 +131,9 @@ fn sign_request_from_filtered_log(log: web3::types::Log) -> anyhow::Result()), - &request.path, + &event.path, ); // Use transaction hash as entropy @@ -150,17 +142,21 @@ fn sign_request_from_filtered_log(log: web3::types::Log) -> anyhow::Result String { pub async fn run( eth: Option, - sign_tx: mpsc::Sender, + sign_tx: mpsc::Sender, node_near_account_id: AccountId, ) -> anyhow::Result<()> { let Some(eth) = eth else { diff --git a/chain-signatures/node/src/kdf.rs b/chain-signatures/node/src/kdf.rs index e4c9cc2d..8e53ff67 100644 --- a/chain-signatures/node/src/kdf.rs +++ b/chain-signatures/node/src/kdf.rs @@ -1,7 +1,8 @@ use anyhow::Context; use hkdf::Hkdf; use k256::{ecdsa::RecoveryId, elliptic_curve::sec1::ToEncodedPoint, AffinePoint, Scalar}; -use mpc_crypto::{kdf::recover, x_coordinate, ScalarExt, SignatureResponse}; +use mpc_crypto::{kdf::recover, x_coordinate, ScalarExt}; +use mpc_primitives::Signature; use near_primitives::hash::CryptoHash; use sha3::Sha3_256; @@ -35,7 +36,7 @@ pub fn into_eth_sig( big_r: &k256::AffinePoint, s: &k256::Scalar, msg_hash: Scalar, -) -> anyhow::Result { +) -> anyhow::Result { let public_key = public_key.to_encoded_point(false); let signature = k256::ecdsa::Signature::from_scalars(x_coordinate(big_r), s) .context("cannot create signature from cait_sith signature")?; @@ -47,7 +48,7 @@ pub fn into_eth_sig( .context("unable to use 0 as recovery_id to recover public key")? .to_encoded_point(false); if public_key == pk0 { - return Ok(SignatureResponse::new(*big_r, *s, 0)); + return Ok(Signature::new(*big_r, *s, 0)); } let pk1 = recover( @@ -58,7 +59,7 @@ pub fn into_eth_sig( .context("unable to use 1 as recovery_id to recover public key")? .to_encoded_point(false); if public_key == pk1 { - return Ok(SignatureResponse::new(*big_r, *s, 1)); + return Ok(Signature::new(*big_r, *s, 1)); } anyhow::bail!("cannot use either recovery id (0 or 1) to recover pubic key") diff --git a/chain-signatures/node/src/protocol/consensus.rs b/chain-signatures/node/src/protocol/consensus.rs index 339f2a9c..71ba703a 100644 --- a/chain-signatures/node/src/protocol/consensus.rs +++ b/chain-signatures/node/src/protocol/consensus.rs @@ -10,7 +10,7 @@ use crate::protocol::presignature::PresignatureManager; use crate::protocol::signature::SignatureManager; use crate::protocol::state::{GeneratingState, ResharingState}; use crate::protocol::triple::TripleManager; -use crate::protocol::SignRequest; +use crate::protocol::IndexedSignRequest; use crate::rpc::NearClient; use crate::storage::presignature_storage::PresignatureStorage; use crate::storage::secret_storage::SecretNodeStorageBox; @@ -33,7 +33,7 @@ pub trait ConsensusCtx { fn near_client(&self) -> &NearClient; fn mpc_contract_id(&self) -> &AccountId; fn my_address(&self) -> &Url; - fn sign_rx(&self) -> Arc>>; + fn sign_rx(&self) -> Arc>>; fn secret_storage(&self) -> &SecretNodeStorageBox; fn triple_storage(&self) -> &TripleStorage; fn presignature_storage(&self) -> &PresignatureStorage; diff --git a/chain-signatures/node/src/protocol/error.rs b/chain-signatures/node/src/protocol/error.rs new file mode 100644 index 00000000..9c9cd9c0 --- /dev/null +++ b/chain-signatures/node/src/protocol/error.rs @@ -0,0 +1,34 @@ +use cait_sith::protocol::{InitializationError, Participant}; +use mpc_primitives::SignId; + +use super::{presignature::PresignatureId, triple::TripleId}; + +#[derive(Debug, thiserror::Error)] +pub enum GenerationError { + #[error("presignature already generated")] + AlreadyGenerated, + #[error("cait-sith initialization error: {0}")] + CaitSithInitializationError(#[from] InitializationError), + #[error("triple storage error: {0}")] + TripleStoreError(String), + #[error("triple {0} is generating")] + TripleIsGenerating(TripleId), + #[error("triple {0} is in garbage collection")] + TripleIsGarbageCollected(TripleId), + #[error("triple access denied: id={0}, {1}")] + TripleDenied(TripleId, &'static str), + #[error("presignature {0} is generating")] + PresignatureIsGenerating(PresignatureId), + #[error("presignature {0} is missing")] + PresignatureIsMissing(PresignatureId), + #[error("presignature {0} is in garbage collection")] + PresignatureIsGarbageCollected(TripleId), + #[error("presignature access denied: id={0}, {1}")] + PresignatureDenied(PresignatureId, &'static str), + #[error("presignature bad parameters")] + PresignatureBadParameters, + #[error("waiting for missing sign request id={0:?}")] + WaitingForIndexer(SignId), + #[error("invalid proposer expected={0:?}, actual={1:?}")] + InvalidProposer(Participant, Participant), +} diff --git a/chain-signatures/node/src/protocol/message.rs b/chain-signatures/node/src/protocol/message.rs index 4ce3ebf8..71b6195e 100644 --- a/chain-signatures/node/src/protocol/message.rs +++ b/chain-signatures/node/src/protocol/message.rs @@ -1,9 +1,8 @@ use super::contract::primitives::{ParticipantMap, Participants}; -use super::presignature::{GenerationError, PresignatureId}; -use super::signature::SignRequestIdentifier; +use super::error::GenerationError; +use super::presignature::PresignatureId; use super::state::{GeneratingState, NodeState, ResharingState, RunningState}; use super::triple::TripleId; -use crate::indexer::ContractSignRequest; use crate::node_client::NodeClient; use crate::protocol::Config; use crate::protocol::MeshState; @@ -12,9 +11,9 @@ use crate::util; use async_trait::async_trait; use cait_sith::protocol::{MessageData, Participant}; -use k256::Scalar; use mpc_contract::config::ProtocolConfig; use mpc_keys::hpke::{self, Ciphered}; +use mpc_primitives::SignId; use near_account_id::AccountId; use near_crypto::Signature; use serde::de::DeserializeOwned; @@ -93,15 +92,9 @@ impl From for Message { #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct SignatureMessage { - #[serde(with = "serde_bytes")] - pub request_id: [u8; 32], + pub id: SignId, pub proposer: Participant, pub presignature_id: PresignatureId, - pub request: ContractSignRequest, - #[serde(with = "cbor_scalar")] - pub epsilon: Scalar, - #[serde(with = "serde_bytes")] - pub entropy: [u8; 32], pub epoch: u64, pub from: Participant, #[serde(with = "serde_bytes")] @@ -168,7 +161,7 @@ pub struct MessageInbox { resharing: HashMap>, triple: HashMap>>, presignature: HashMap>>, - signature: HashMap>>, + signature: HashMap>>, } impl MessageInbox { @@ -198,11 +191,7 @@ impl MessageInbox { .signature .entry(message.epoch) .or_default() - .entry(SignRequestIdentifier::new( - message.request_id, - message.epsilon, - message.request.payload, - )) + .entry(message.id.clone()) .or_default() .push_back(message), Message::Unknown(entries) => { @@ -1095,40 +1084,6 @@ fn timeout(msg: &Message, cfg: &ProtocolConfig) -> Duration { } } -/// Scalar module for any scalars to be sent through messaging other nodes. -/// There's an issue with serializing with ciborium when it comes to -/// forward and backward compatibility, so we need to implement our own -/// custom serialization here. -pub mod cbor_scalar { - use k256::elliptic_curve::bigint::Encoding as _; - use k256::elliptic_curve::scalar::FromUintUnchecked as _; - use k256::Scalar; - use serde::{de, Deserialize as _, Deserializer, Serialize, Serializer}; - - pub fn serialize(scalar: &Scalar, ser: S) -> Result { - let num = k256::U256::from(scalar); - let bytes = num.to_le_bytes(); - serde_bytes::Bytes::new(&bytes).serialize(ser) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { - let bytes = match ciborium::Value::deserialize(deserializer)? { - ciborium::Value::Bytes(bytes) if bytes.len() != 32 => { - return Err(de::Error::custom("expected 32 bytes for Scalar")) - } - ciborium::Value::Bytes(bytes) => bytes, - _ => return Err(de::Error::custom("expected ciborium::Value::Bytes")), - }; - - let mut buf = [0u8; 32]; - buf.copy_from_slice(&bytes[0..32]); - - let num = k256::U256::from_le_bytes(buf); - let scalar = k256::Scalar::from_uint_unchecked(num); - Ok(scalar) - } -} - fn cbor_to_bytes(value: &T) -> Result, MessageError> { let mut buf = Vec::new(); ciborium::into_writer(value, &mut buf) @@ -1158,17 +1113,14 @@ const fn cbor_name(value: &ciborium::Value) -> &'static str { #[cfg(test)] mod tests { use cait_sith::protocol::Participant; - use k256::Scalar; use mpc_keys::hpke::{self, Ciphered}; + use mpc_primitives::SignId; use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use crate::{ - indexer::ContractSignRequest, - protocol::{ - contract::primitives::{ParticipantMap, Participants}, - message::{GeneratingMessage, Message, SignatureMessage, SignedMessage, TripleMessage}, - ParticipantInfo, - }, + use crate::protocol::{ + contract::primitives::{ParticipantMap, Participants}, + message::{GeneratingMessage, Message, SignatureMessage, SignedMessage, TripleMessage}, + ParticipantInfo, }; #[test] @@ -1344,18 +1296,10 @@ mod tests { data: vec![8; 512], }), Message::Signature(SignatureMessage { + id: SignId::new([7; 32]), proposer: from, presignature_id: 1234, - request_id: [7; 32], epoch: 0, - request: ContractSignRequest { - payload: Scalar::ZERO, - path: "test-something".to_string(), - key_version: 1, - chain: crate::protocol::Chain::NEAR, - }, - epsilon: Scalar::ONE, - entropy: [9; 32], from, data: vec![78; 1222], timestamp: 1234567, diff --git a/chain-signatures/node/src/protocol/mod.rs b/chain-signatures/node/src/protocol/mod.rs index b2314086..01d1c2a3 100644 --- a/chain-signatures/node/src/protocol/mod.rs +++ b/chain-signatures/node/src/protocol/mod.rs @@ -2,6 +2,7 @@ mod cryptography; pub mod consensus; pub mod contract; +pub mod error; pub mod message; pub mod presignature; pub mod signature; @@ -13,7 +14,7 @@ pub use contract::primitives::ParticipantInfo; pub use contract::ProtocolState; pub use cryptography::CryptographicError; pub use message::{Message, MessageChannel}; -pub use signature::{SignQueue, SignRequest}; +pub use signature::{IndexedSignRequest, SignQueue}; pub use state::NodeState; pub use sysinfo::{Components, CpuRefreshKind, Disks, RefreshKind, System}; @@ -44,7 +45,7 @@ struct Ctx { mpc_contract_id: AccountId, near: NearClient, rpc_channel: RpcChannel, - sign_rx: Arc>>, + sign_rx: Arc>>, secret_storage: SecretNodeStorageBox, triple_storage: TripleStorage, presignature_storage: PresignatureStorage, @@ -67,7 +68,7 @@ impl ConsensusCtx for &mut MpcSignProtocol { &self.ctx.my_address } - fn sign_rx(&self) -> Arc>> { + fn sign_rx(&self) -> Arc>> { self.ctx.sign_rx.clone() } @@ -130,7 +131,7 @@ impl MpcSignProtocol { near: NearClient, rpc_channel: RpcChannel, channel: MessageChannel, - sign_rx: mpsc::Receiver, + sign_rx: mpsc::Receiver, secret_storage: SecretNodeStorageBox, triple_storage: TripleStorage, presignature_storage: PresignatureStorage, diff --git a/chain-signatures/node/src/protocol/presignature.rs b/chain-signatures/node/src/protocol/presignature.rs index 02eb24c1..7ff53eaf 100644 --- a/chain-signatures/node/src/protocol/presignature.rs +++ b/chain-signatures/node/src/protocol/presignature.rs @@ -1,8 +1,8 @@ use super::message::{MessageChannel, PresignatureMessage}; -use super::signature::SignRequestIdentifier; use super::state::RunningState; use super::triple::{Triple, TripleId, TripleManager}; use crate::protocol::contract::primitives::Participants; +use crate::protocol::error::GenerationError; use crate::storage::presignature_storage::PresignatureStorage; use crate::types::{PresignatureProtocol, SecretKeyShare}; use crate::util::AffinePointExt; @@ -127,36 +127,6 @@ impl PresignatureGenerator { } } -#[derive(Debug, thiserror::Error)] -pub enum GenerationError { - #[error("presignature already generated")] - AlreadyGenerated, - #[error("cait-sith initialization error: {0}")] - CaitSithInitializationError(#[from] InitializationError), - #[error("triple storage error: {0}")] - TripleStoreError(String), - #[error("triple {0} is generating")] - TripleIsGenerating(TripleId), - #[error("triple {0} is in garbage collection")] - TripleIsGarbageCollected(TripleId), - #[error("triple access denied: id={0}, {1}")] - TripleDenied(TripleId, &'static str), - #[error("presignature {0} is generating")] - PresignatureIsGenerating(PresignatureId), - #[error("presignature {0} is missing")] - PresignatureIsMissing(PresignatureId), - #[error("presignature {0} is in garbage collection")] - PresignatureIsGarbageCollected(TripleId), - #[error("presignature access denied: id={0}, {1}")] - PresignatureDenied(PresignatureId, &'static str), - #[error("presignature bad parameters")] - PresignatureBadParameters, - #[error("waiting for missing sign request id={0:?}")] - WaitingForIndexer(SignRequestIdentifier), - #[error("invalid proposer expected={0:?}, actual={1:?}")] - InvalidProposer(Participant, Participant), -} - /// Abstracts how triples are generated by providing a way to request a new triple that will be /// complete some time in the future and a way to take an already generated triple. pub struct PresignatureManager { diff --git a/chain-signatures/node/src/protocol/signature.rs b/chain-signatures/node/src/protocol/signature.rs index c4b4cd64..6c0a503a 100644 --- a/chain-signatures/node/src/protocol/signature.rs +++ b/chain-signatures/node/src/protocol/signature.rs @@ -1,9 +1,9 @@ use super::contract::primitives::Participants; -use super::presignature::{GenerationError, Presignature, PresignatureId, PresignatureManager}; use super::state::RunningState; -use crate::indexer::ContractSignRequest; use crate::kdf::derive_delta; +use crate::protocol::error::GenerationError; use crate::protocol::message::{MessageChannel, SignatureMessage}; +use crate::protocol::presignature::{Presignature, PresignatureId, PresignatureManager}; use crate::protocol::Chain; use crate::rpc::RpcChannel; use crate::types::SignatureProtocol; @@ -12,11 +12,10 @@ use crate::util::AffinePointExt; use cait_sith::protocol::{Action, InitializationError, Participant, ProtocolError}; use cait_sith::{FullSignature, PresignOutput}; use chrono::Utc; -use k256::{Scalar, Secp256k1}; +use k256::Secp256k1; use mpc_contract::config::ProtocolConfig; -use mpc_contract::primitives::SignatureRequest; -use mpc_crypto::SerializableScalar; use mpc_crypto::{derive_key, PublicKey}; +use mpc_primitives::{SignArgs, SignId}; use rand::rngs::StdRng; use rand::seq::{IteratorRandom, SliceRandom}; use rand::SeedableRng; @@ -28,34 +27,45 @@ use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::{mpsc, RwLock}; use near_account_id::AccountId; -use near_primitives::hash::CryptoHash; - -pub type ReceiptId = near_primitives::hash::CryptoHash; /// This is the maximum amount of sign requests that we can accept in the network. const MAX_SIGN_REQUESTS: usize = 1024; +/// All relevant info pertaining to an Indexed sign request from an indexer. +#[derive(Debug, Clone, PartialEq)] +pub struct IndexedSignRequest { + pub id: SignId, + pub args: SignArgs, + pub chain: Chain, + pub timestamp: Instant, +} + +/// The sign request for the node to process. This contains relevant info for the node +/// to generate a signature such as what has been indexed and what the node needs to maintain +/// metadata-wise to generate the signature. +#[derive(Debug, Clone, PartialEq)] pub struct SignRequest { - pub request_id: [u8; 32], - pub request: ContractSignRequest, - pub epsilon: Scalar, - pub entropy: [u8; 32], - pub time_added: Instant, + pub indexed: IndexedSignRequest, + pub proposer: Participant, + pub participants: Vec, } pub struct SignQueue { me: Participant, - sign_rx: Arc>>, - my_requests: VecDeque<(SignRequest, Participant, Vec)>, - other_requests: HashMap)>, + sign_rx: Arc>>, + my_requests: VecDeque, + other_requests: HashMap, } impl SignQueue { - pub fn channel() -> (mpsc::Sender, mpsc::Receiver) { + pub fn channel() -> ( + mpsc::Sender, + mpsc::Receiver, + ) { mpsc::channel(MAX_SIGN_REQUESTS) } - pub fn new(me: Participant, sign_rx: Arc>>) -> Self { + pub fn new(me: Participant, sign_rx: Arc>>) -> Self { Self { me, sign_rx, @@ -64,6 +74,14 @@ impl SignQueue { } } + pub fn len_mine(&self) -> usize { + self.my_requests.len() + } + + pub fn is_empty_mine(&self) -> bool { + self.len_mine() == 0 + } + pub fn len(&self) -> usize { self.my_requests.len() + self.other_requests.len() } @@ -79,7 +97,7 @@ impl SignQueue { my_account_id: &AccountId, ) { let mut sign_rx = self.sign_rx.write().await; - while let Ok(request) = { + while let Ok(indexed) = { match sign_rx.try_recv() { err @ Err(TryRecvError::Disconnected) => { tracing::error!("sign queue channel disconnected"); @@ -88,15 +106,15 @@ impl SignQueue { other => other, } } { - let mut rng = StdRng::from_seed(request.entropy); + let mut rng = StdRng::from_seed(indexed.args.entropy); let subset = stable.keys().cloned().choose_multiple(&mut rng, threshold); let in_subset = subset.contains(&self.me); let proposer = *subset.choose(&mut rng).unwrap(); let is_mine = proposer == self.me; - let request_id = CryptoHash(request.request_id); + let sign_id = indexed.id.clone(); tracing::info!( - ?request_id, + ?sign_id, ?subset, ?proposer, in_subset, @@ -104,110 +122,99 @@ impl SignQueue { "sign queue: organizing request" ); - let sign_id = SignRequestIdentifier::new( - request.request_id, - request.epsilon, - request.request.payload, - ); - if in_subset { tracing::info!( - ?request_id, + ?sign_id, "saving sign request: node is in the signer subset" ); + let request = SignRequest { + indexed, + proposer, + participants: subset, + }; if is_mine { crate::metrics::NUM_SIGN_REQUESTS_MINE .with_label_values(&[my_account_id.as_str()]) .inc(); - self.my_requests.push_back((request, proposer, subset)); + self.my_requests.push_back(request); } else { - self.other_requests - .insert(sign_id, (request, proposer, subset)); + self.other_requests.insert(sign_id, request); } } else { tracing::info!( - ?request_id, + ?sign_id, "skipping sign request: node is NOT in the signer subset" ); } } } - pub fn take_my_requests(&mut self) -> VecDeque<(SignRequest, Participant, Vec)> { - std::mem::take(&mut self.my_requests) + pub fn push_failed(&mut self, request: SignRequest) { + // NOTE: this prioritizes old requests first then tries to do new ones if there's enough presignatures. + // TODO: we need to decide how to prioritize certain requests over others such as with gas or time of + // when the request made it into the NEAR network. + // issue: https://github.com/near/mpc-recovery/issues/596 + if request.proposer == self.me { + self.my_requests.push_front(request); + } else { + self.other_requests + .insert(request.indexed.id.clone(), request); + } } - pub fn insert_mine( - &mut self, - requests: VecDeque<(SignRequest, Participant, Vec)>, - ) { - self.my_requests = requests; + pub fn take_mine(&mut self) -> Option { + self.my_requests.pop_front() } - pub fn take( - &mut self, - id: &SignRequestIdentifier, - ) -> Option<(SignRequest, Participant, Vec)> { + pub fn take(&mut self, id: &SignId) -> Option { self.other_requests.remove(id) } + + pub fn expire(&mut self, cfg: &ProtocolConfig) { + self.other_requests.retain(|_, request| { + request.indexed.timestamp.elapsed() + < Duration::from_millis(cfg.signature.generation_timeout_total) + }); + } } /// An ongoing signature generator. pub struct SignatureGenerator { pub protocol: SignatureProtocol, - pub participants: Vec, - pub proposer: Participant, pub presignature_id: PresignatureId, - pub request: ContractSignRequest, - pub epsilon: Scalar, - pub request_id: [u8; 32], - pub entropy: [u8; 32], - pub sign_request_timestamp: Instant, - pub generator_timestamp: Instant, + pub request: SignRequest, + pub timestamp: Instant, pub timeout: Duration, pub timeout_total: Duration, } impl SignatureGenerator { - #[allow(clippy::too_many_arguments)] pub fn new( protocol: SignatureProtocol, - participants: Vec, - proposer: Participant, presignature_id: PresignatureId, - request: ContractSignRequest, - epsilon: Scalar, - request_id: [u8; 32], - entropy: [u8; 32], - sign_request_timestamp: Instant, + request: SignRequest, cfg: &ProtocolConfig, ) -> Self { Self { protocol, - participants, - proposer, presignature_id, request, - epsilon, - request_id, - entropy, - sign_request_timestamp, - generator_timestamp: Instant::now(), + timestamp: Instant::now(), timeout: Duration::from_millis(cfg.signature.generation_timeout), timeout_total: Duration::from_millis(cfg.signature.generation_timeout_total), } } pub fn poke(&mut self) -> Result>, ProtocolError> { - if self.sign_request_timestamp.elapsed() > self.timeout_total { + if self.request.indexed.timestamp.elapsed() > self.timeout_total { let msg = "signature protocol timed out completely"; tracing::warn!(msg); return Err(ProtocolError::Other(anyhow::anyhow!(msg).into())); } - if self.generator_timestamp.elapsed() > self.timeout { - tracing::warn!(self.presignature_id, "signature protocol timed out"); + if self.timestamp.elapsed() > self.timeout { + tracing::warn!(sign_id = ?self.request.indexed.id, "signature protocol timed out"); return Err(ProtocolError::Other( anyhow::anyhow!("signature protocol timeout").into(), )); @@ -217,50 +224,11 @@ impl SignatureGenerator { } } -/// Generator for signature thas has failed. Only retains essential information -/// for starting up this failed signature once again. -pub struct GenerationRequest { - pub proposer: Participant, - pub participants: Vec, - pub request: ContractSignRequest, - pub epsilon: Scalar, - pub request_id: [u8; 32], - pub entropy: [u8; 32], - pub sign_request_timestamp: Instant, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SignRequestIdentifier { - pub request_id: [u8; 32], - pub epsilon: Scalar, - pub payload: Scalar, -} - -impl std::hash::Hash for SignRequestIdentifier { - fn hash(&self, state: &mut H) { - self.request_id.hash(state); - self.epsilon.to_bytes().hash(state); - self.payload.to_bytes().hash(state); - } -} - -impl SignRequestIdentifier { - pub fn new(request_id: [u8; 32], epsilon: Scalar, payload: Scalar) -> Self { - Self { - request_id, - epsilon, - payload, - } - } -} - pub struct SignatureManager { /// Ongoing signature generation protocols. - generators: HashMap, - /// Failed signatures awaiting to be retried. - failed: VecDeque<(SignRequestIdentifier, GenerationRequest)>, + generators: HashMap, /// Set of completed signatures - completed: HashMap, + completed: HashMap, /// Sign queue that maintains all requests coming in from indexer. sign_queue: SignQueue, @@ -271,34 +239,6 @@ pub struct SignatureManager { epoch: u64, } -pub struct ToPublish { - pub request_id: [u8; 32], - pub request: SignatureRequest, - pub time_added: Instant, - pub signature: FullSignature, - pub retry_count: u8, - pub chain: Chain, -} - -impl ToPublish { - pub fn new( - request_id: [u8; 32], - request: SignatureRequest, - time_added: Instant, - signature: FullSignature, - chain: Chain, - ) -> ToPublish { - ToPublish { - request_id, - request, - time_added, - signature, - retry_count: 0, - chain, - } - } -} - impl SignatureManager { pub fn new( me: Participant, @@ -306,11 +246,10 @@ impl SignatureManager { threshold: usize, public_key: PublicKey, epoch: u64, - sign_rx: Arc>>, + sign_rx: Arc>>, ) -> Self { Self { generators: HashMap::new(), - failed: VecDeque::new(), completed: HashMap::new(), sign_queue: SignQueue::new(me, sign_rx), me, @@ -321,128 +260,72 @@ impl SignatureManager { } } - pub fn failed_len(&self) -> usize { - self.failed.len() - } - pub fn me(&self) -> Participant { self.me } - #[allow(clippy::result_large_err)] fn generate_internal( me: Participant, public_key: PublicKey, presignature: Presignature, - req: GenerationRequest, + request: SignRequest, cfg: &ProtocolConfig, - ) -> Result { - let GenerationRequest { - proposer, + ) -> Result { + let SignRequest { participants, - request, - epsilon, - request_id, - entropy, - sign_request_timestamp, - } = req; + indexed, + .. + } = &request; + let IndexedSignRequest { + id: SignId { request_id }, + args, + .. + } = indexed; + let PresignOutput { big_r, k, sigma } = presignature.output; - let delta = derive_delta(request_id, entropy, big_r); + let delta = derive_delta(*request_id, args.entropy, big_r); // TODO: Check whether it is okay to use invert_vartime instead let output: PresignOutput = PresignOutput { big_r: (big_r * delta).to_affine(), k: k * delta.invert().unwrap(), - sigma: (sigma + epsilon * k) * delta.invert().unwrap(), + sigma: (sigma + args.epsilon * k) * delta.invert().unwrap(), }; - let presignature_id = presignature.id; - let protocol = Box::new( - cait_sith::sign( - &participants, - me, - derive_key(public_key, epsilon), - output, - request.payload, - ) - .map_err(|err| (presignature, err))?, - ); + let protocol = Box::new(cait_sith::sign( + participants, + me, + derive_key(public_key, args.epsilon), + output, + args.payload, + )?); Ok(SignatureGenerator::new( protocol, - participants.to_vec(), - proposer, - presignature_id, + presignature.id, request, - epsilon, - request_id, - entropy, - sign_request_timestamp, cfg, )) } - #[allow(clippy::result_large_err)] - fn retry_failed_generation( - &mut self, - sign_id: SignRequestIdentifier, - req: GenerationRequest, - presignature: Presignature, - cfg: &ProtocolConfig, - ) -> Result<(), (Presignature, InitializationError)> { - tracing::info!( - ?sign_id, - participants = ?req.participants, - "restarting failed protocol to generate signature" - ); - let generator = Self::generate_internal(self.me, self.public_key, presignature, req, cfg)?; - crate::metrics::NUM_TOTAL_HISTORICAL_SIGNATURE_GENERATORS - .with_label_values(&[self.my_account_id.as_str()]) - .inc(); - self.generators.insert(sign_id, generator); - Ok(()) - } - /// Starts a new presignature generation protocol. - #[allow(clippy::too_many_arguments)] - #[allow(clippy::result_large_err)] pub fn generate( &mut self, - participants: &Participants, - request_id: [u8; 32], presignature: Presignature, - request: ContractSignRequest, - epsilon: Scalar, - entropy: [u8; 32], - sign_request_timestamp: Instant, + request: SignRequest, cfg: &ProtocolConfig, - ) -> Result<(), (Presignature, InitializationError)> { - let sign_request_identifier = - SignRequestIdentifier::new(request_id, epsilon, request.payload); - let participants = participants.keys_vec(); + ) -> Result<(), InitializationError> { + let sign_id = request.indexed.id.clone(); tracing::info!( - ?sign_request_identifier, + ?sign_id, me = ?self.me, presignature_id = presignature.id, - ?participants, + participants = ?request.participants, "starting protocol to generate a new signature", ); - let generator = Self::generate_internal( - self.me, - self.public_key, - presignature, - GenerationRequest { - proposer: self.me, - participants, - request, - epsilon, - request_id, - entropy, - sign_request_timestamp, - }, - cfg, - )?; + let generator = + Self::generate_internal(self.me, self.public_key, presignature, request, cfg)?; crate::metrics::NUM_TOTAL_HISTORICAL_SIGNATURE_GENERATORS .with_label_values(&[self.my_account_id.as_str()]) .inc(); - self.generators.insert(sign_request_identifier, generator); + self.generators.insert(sign_id, generator); Ok(()) } @@ -454,7 +337,7 @@ impl SignatureManager { // TODO: What if the presignature completed generation and is already spent? pub async fn get_or_start_protocol( &mut self, - sign_id: &SignRequestIdentifier, + sign_id: &SignId, proposer: Participant, presignature_id: PresignatureId, cfg: &ProtocolConfig, @@ -474,11 +357,11 @@ impl SignatureManager { Entry::Occupied(entry) => return Ok(&mut entry.into_mut().protocol), }; - let Some((request, our_proposer, subset)) = self.sign_queue.take(sign_id) else { + let Some(request) = self.sign_queue.take(sign_id) else { return Err(GenerationError::WaitingForIndexer(sign_id.clone())); }; - if proposer != our_proposer { - return Err(GenerationError::InvalidProposer(proposer, our_proposer)); + if proposer != request.proposer { + return Err(GenerationError::InvalidProposer(proposer, request.proposer)); } tracing::info!(?sign_id, me = ?self.me, presignature_id, "joining protocol to generate a new signature"); @@ -499,32 +382,19 @@ impl SignatureManager { Err(err) => return Err(err), }; tracing::info!(me = ?self.me, presignature_id, "found presignature: ready to start signature generation"); - let generator = match Self::generate_internal( - self.me, - self.public_key, - presignature, - GenerationRequest { - proposer, - participants: subset, - request: request.request, - epsilon: request.epsilon, - entropy: request.entropy, - request_id: sign_id.request_id, - sign_request_timestamp: Instant::now(), - }, - cfg, - ) { - Ok(generator) => generator, - Err((presignature, err @ InitializationError::BadParameters(_))) => { - tracing::warn!( - ?sign_id, - presignature.id, - ?err, - "failed to start signature generation" - ); - return Err(GenerationError::CaitSithInitializationError(err)); - } - }; + let generator = + match Self::generate_internal(self.me, self.public_key, presignature, request, cfg) { + Ok(generator) => generator, + Err(err @ InitializationError::BadParameters(_)) => { + tracing::warn!( + ?sign_id, + presignature_id, + ?err, + "failed to start signature generation" + ); + return Err(GenerationError::CaitSithInitializationError(err)); + } + }; let generator = entry.insert(generator); crate::metrics::NUM_TOTAL_HISTORICAL_SIGNATURE_GENERATORS .with_label_values(&[self.my_account_id.as_str()]) @@ -535,35 +405,25 @@ impl SignatureManager { /// Pokes all of the ongoing generation protocols to completion pub async fn poke(&mut self, message: MessageChannel, rpc: RpcChannel) { let mut remove = Vec::new(); - for (sign_request_id, generator) in self.generators.iter_mut() { + let mut failed = Vec::new(); + for (sign_id, generator) in self.generators.iter_mut() { loop { let action = match generator.poke() { Ok(action) => action, Err(err) => { - if generator.proposer == self.me { - if generator.sign_request_timestamp.elapsed() < generator.timeout_total + remove.push(sign_id.clone()); + + if generator.request.proposer == self.me { + if generator.request.indexed.timestamp.elapsed() + < generator.timeout_total { + failed.push(sign_id.clone()); tracing::warn!(?err, "signature failed to be produced; pushing request back into failed queue"); crate::metrics::SIGNATURE_GENERATOR_FAILURES .with_label_values(&[self.my_account_id.as_str()]) .inc(); - // only retry the signature generation if it was initially proposed by us. We do not - // want any nodes to be proposing the same signature multiple times. - self.failed.push_back(( - sign_request_id.clone(), - GenerationRequest { - proposer: generator.proposer, - participants: generator.participants.clone(), - request: generator.request.clone(), - epsilon: generator.epsilon, - request_id: generator.request_id, - entropy: generator.entropy, - sign_request_timestamp: generator.sign_request_timestamp, - }, - )); } else { - self.completed - .insert(sign_request_id.clone(), Instant::now()); + self.completed.insert(sign_id.clone(), Instant::now()); crate::metrics::SIGNATURE_GENERATOR_FAILURES .with_label_values(&[self.my_account_id.as_str()]) .inc(); @@ -576,7 +436,6 @@ impl SignatureManager { ); } } - remove.push(sign_request_id.clone()); break; } }; @@ -587,7 +446,7 @@ impl SignatureManager { break; } Action::SendMany(data) => { - for to in generator.participants.iter() { + for to in generator.request.participants.iter() { if *to == self.me { continue; } @@ -596,12 +455,9 @@ impl SignatureManager { self.me, *to, SignatureMessage { - request_id: sign_request_id.request_id, - proposer: generator.proposer, + id: sign_id.clone(), + proposer: generator.request.proposer, presignature_id: generator.presignature_id, - request: generator.request.clone(), - epsilon: generator.epsilon, - entropy: generator.entropy, epoch: self.epoch, from: self.me, data: data.clone(), @@ -617,12 +473,9 @@ impl SignatureManager { self.me, to, SignatureMessage { - request_id: sign_request_id.request_id, - proposer: generator.proposer, + id: sign_id.clone(), + proposer: generator.request.proposer, presignature_id: generator.presignature_id, - request: generator.request.clone(), - epsilon: generator.epsilon, - entropy: generator.entropy, epoch: self.epoch, from: self.me, data, @@ -633,45 +486,37 @@ impl SignatureManager { } Action::Return(output) => { tracing::info!( - ?sign_request_id, + ?sign_id, me = ?self.me, presignature_id = generator.presignature_id, big_r = ?output.big_r.to_base58(), s = ?output.s, - elapsed = ?generator.generator_timestamp.elapsed(), + elapsed = ?generator.timestamp.elapsed(), "completed signature generation" ); crate::metrics::SIGN_GENERATION_LATENCY .with_label_values(&[self.my_account_id.as_str()]) - .observe(generator.generator_timestamp.elapsed().as_secs_f64()); - - self.completed - .insert(sign_request_id.clone(), Instant::now()); - let request = SignatureRequest { - epsilon: SerializableScalar { - scalar: generator.epsilon, - }, - payload_hash: generator.request.payload.into(), - }; - if generator.proposer == self.me { - let to_publish = ToPublish::new( - sign_request_id.request_id, - request, - generator.sign_request_timestamp, - output, - generator.request.chain, - ); - tokio::spawn(rpc.clone().publish(self.public_key, to_publish)); + .observe(generator.timestamp.elapsed().as_secs_f64()); + + if generator.request.proposer == self.me { + rpc.publish(self.public_key, generator.request.clone(), output); } + self.completed.insert(sign_id.clone(), Instant::now()); // Do not retain the protocol - remove.push(sign_request_id.clone()); + remove.push(sign_id.clone()); } } } } - for sign_request_id in remove { - self.generators.remove(&sign_request_id); + for id in failed { + if let Some(generator) = self.generators.remove(&id) { + self.sign_queue.push_failed(generator.request); + } + } + + for id in remove { + self.generators.remove(&id); } } @@ -691,71 +536,31 @@ impl SignatureManager { return; } + self.sign_queue.expire(cfg); self.sign_queue .organize(self.threshold, stable, &self.my_account_id) .await; crate::metrics::SIGN_QUEUE_SIZE .with_label_values(&[self.my_account_id.as_str()]) .set(self.sign_queue.len() as i64); - let mut my_requests = self.sign_queue.take_my_requests(); crate::metrics::SIGN_QUEUE_MINE_SIZE .with_label_values(&[self.my_account_id.as_str()]) - .set(my_requests.len() as i64); + .set(self.sign_queue.len_mine() as i64); - while let Some(mut presignature) = { - if self.failed.is_empty() && my_requests.is_empty() { + while let Some(presignature) = { + if self.sign_queue.is_empty_mine() { None } else { presignature_manager.take_mine().await } } { - // NOTE: this prioritizes old requests first then tries to do new ones if there's enough presignatures. - // TODO: we need to decide how to prioritize certain requests over others such as with gas or time of - // when the request made it into the NEAR network. - // issue: https://github.com/near/mpc-recovery/issues/596 - if let Some((sign_id, failed_req)) = self.failed.pop_front() { - let participants = - stable.intersection(&[&presignature.participants, &failed_req.participants]); - if participants.len() < self.threshold { - tracing::warn!( - participants = ?participants.keys_vec(), - "intersection of stable participants and presignature participants is less than threshold, trashing presignature" - ); - // TODO: do not insert back presignature when we have a clear model for data consistency - // between nodes and utilizing only presignatures that meet threshold requirements. - presignature_manager.insert(presignature, true, true).await; - continue; - } - - if let Err((presignature, InitializationError::BadParameters(err))) = - self.retry_failed_generation(sign_id.clone(), failed_req, presignature, cfg) - { - tracing::warn!( - ?sign_id, - presignature.id, - ?err, - "failed to retry signature generation: trashing presignature" - ); - continue; - } - - if my_requests.is_empty() { - continue; - } - - if let Some(another_presignature) = presignature_manager.take_mine().await { - presignature = another_presignature; - } else { - break; - } - } - - let Some((my_request, _proposer, subset)) = my_requests.pop_front() else { + let Some(my_request) = self.sign_queue.take_mine() else { tracing::warn!("unexpected state, no more requests to handle"); continue; }; - let participants = stable.intersection(&[&presignature.participants, &subset]); + let participants = + stable.intersection(&[&presignature.participants, &my_request.participants]); if participants.len() < self.threshold { tracing::warn!( participants = ?participants.keys_vec(), @@ -767,26 +572,20 @@ impl SignatureManager { continue; } - if let Err((presignature, InitializationError::BadParameters(err))) = self.generate( - &participants, - my_request.request_id, - presignature, - my_request.request, - my_request.epsilon, - my_request.entropy, - my_request.time_added, - cfg, - ) { - tracing::warn!(request_id = ?CryptoHash(my_request.request_id), presignature.id, ?err, "failed to start signature generation: trashing presignature"); + let sign_id = my_request.indexed.id.clone(); + let presignature_id = presignature.id; + if let Err(InitializationError::BadParameters(err)) = + self.generate(presignature, my_request, cfg) + { + tracing::warn!( + ?sign_id, + presignature_id, + ?err, + "failed to start signature generation: trashing presignature" + ); continue; } } - - // We do not have enough presignature stockpile and the taken requests need to be fulfilled, - // so insert it back into the sign queue to be fulfilled in the next iteration. - if !my_requests.is_empty() { - self.sign_queue.insert_mine(my_requests); - } } /// Garbage collect all the completed signatures. @@ -804,7 +603,7 @@ impl SignatureManager { } } - pub fn refresh_gc(&mut self, id: &SignRequestIdentifier) -> bool { + pub fn refresh_gc(&mut self, id: &SignId) -> bool { let entry = self .completed .entry(id.clone()) diff --git a/chain-signatures/node/src/protocol/triple.rs b/chain-signatures/node/src/protocol/triple.rs index 0b057c28..f22d673a 100644 --- a/chain-signatures/node/src/protocol/triple.rs +++ b/chain-signatures/node/src/protocol/triple.rs @@ -1,7 +1,7 @@ use super::contract::primitives::Participants; use super::cryptography::CryptographicError; use super::message::{MessageChannel, TripleMessage}; -use super::presignature::GenerationError; +use crate::protocol::error::GenerationError; use crate::storage::triple_storage::TripleStorage; use crate::types::TripleProtocol; use crate::util::AffinePointExt; diff --git a/chain-signatures/node/src/rpc.rs b/chain-signatures/node/src/rpc.rs index b75a27b3..d50f97d1 100644 --- a/chain-signatures/node/src/rpc.rs +++ b/chain-signatures/node/src/rpc.rs @@ -1,19 +1,20 @@ use crate::config::{Config, ContractConfig, NetworkConfig}; use crate::indexer_eth::EthConfig; -use crate::protocol::signature::ToPublish; +use crate::protocol::signature::SignRequest; use crate::protocol::{Chain, ProtocolState}; use crate::util::AffinePointExt as _; -use mpc_contract::primitives::SignatureRequest; -use mpc_crypto::SignatureResponse; +use cait_sith::FullSignature; +use k256::Secp256k1; use mpc_keys::hpke; +use mpc_primitives::SignId; +use mpc_primitives::Signature; use k256::elliptic_curve::point::AffineCoordinates; use k256::elliptic_curve::sec1::ToEncodedPoint; use near_account_id::AccountId; use near_crypto::InMemorySigner; use near_fetch::result::ExecutionFinalResult; -use near_primitives::hash::CryptoHash; use serde_json::json; use std::str::FromStr as _; use std::sync::Arc; @@ -25,7 +26,7 @@ use web3::ethabi::Token; use web3::types::U256; /// The maximum amount of times to retry publishing a signature. -const MAX_PUBLISH_RETRY: u8 = 6; +const MAX_PUBLISH_RETRY: usize = 6; /// The maximum number of concurrent RPC requests the system can make const MAX_CONCURRENT_RPC_REQUESTS: usize = 1024; /// The update interval to fetch and update the contract state and config @@ -33,8 +34,10 @@ const UPDATE_INTERVAL: Duration = Duration::from_secs(3); struct PublishAction { public_key: mpc_crypto::PublicKey, - to_publish: ToPublish, + request: SignRequest, + output: FullSignature, timestamp: Instant, + retry_count: usize, } enum RpcAction { @@ -47,18 +50,28 @@ pub struct RpcChannel { } impl RpcChannel { - pub async fn publish(self, public_key: mpc_crypto::PublicKey, to_publish: ToPublish) { - if let Err(err) = self - .tx - .send(RpcAction::Publish(PublishAction { - public_key, - to_publish, - timestamp: Instant::now(), - })) - .await - { - tracing::error!(%err, "failed to send publish action"); - } + pub fn publish( + &self, + public_key: mpc_crypto::PublicKey, + request: SignRequest, + output: FullSignature, + ) { + let rpc = self.clone(); + tokio::spawn(async move { + if let Err(err) = rpc + .tx + .send(RpcAction::Publish(PublishAction { + public_key, + request, + output, + timestamp: Instant::now(), + retry_count: 0, + })) + .await + { + tracing::error!(%err, "failed to send publish action"); + } + }); } } @@ -106,7 +119,7 @@ impl RpcExecutor { }; let task = match action { RpcAction::Publish(action) => { - execute_publish(self.client(&action.to_publish.chain), action) + execute_publish(self.client(&action.request.indexed.chain), action) } }; tokio::spawn(task); @@ -262,14 +275,14 @@ impl NearClient { pub async fn call_respond( &self, - request: &SignatureRequest, - response: &SignatureResponse, + id: &SignId, + response: &Signature, ) -> Result { self.client .call(&self.signer, &self.contract_id, "respond") .args_json(json!({ - "request": request, - "response": response, + "sign_id": id, + "signature": response, })) .max_gas() .transact() @@ -336,30 +349,24 @@ async fn update_config(near: NearClient, config: Arc>) { /// Publish the signature and retry if it fails async fn execute_publish(client: ChainClient, mut action: PublishAction) { - let ToPublish { - request_id, - request, - signature, - chain, - .. - } = &action.to_publish; - tracing::info!( - ?chain, - request_id = ?CryptoHash(*request_id), + sign_id = ?action.request.indexed.id, + chain = ?action.request.indexed.chain, started_at = ?action.timestamp.elapsed(), "trying to publish signature", ); - let expected_public_key = mpc_crypto::derive_key(action.public_key, request.epsilon.scalar); + let expected_public_key = + mpc_crypto::derive_key(action.public_key, action.request.indexed.args.epsilon); + // We do this here, rather than on the client side, so we can use the ecrecover system function on NEAR to validate our signature let Ok(signature) = crate::kdf::into_eth_sig( &expected_public_key, - &signature.big_r, - &signature.s, - request.payload_hash.scalar, + &action.output.big_r, + &action.output.s, + action.request.indexed.args.payload, ) else { tracing::error!( - request_id = ?CryptoHash(*request_id), + sign_id = ?action.request.indexed.id, "failed to generate a recovery id; trashing publish request", ); return; @@ -368,12 +375,12 @@ async fn execute_publish(client: ChainClient, mut action: PublishAction) { loop { let publish = match &client { ChainClient::Near(near) => { - try_publish_near(near, &action.to_publish, &action.timestamp, &signature) + try_publish_near(near, &action, &action.timestamp, &signature) .await .map_err(|_| ()) } ChainClient::Ethereum(eth) => { - try_publish_eth(eth, &action.to_publish, &action.timestamp, &signature).await + try_publish_eth(eth, &action, &action.timestamp, &signature).await } ChainClient::Err(msg) => { tracing::warn!(msg, "no client for chain"); @@ -384,19 +391,19 @@ async fn execute_publish(client: ChainClient, mut action: PublishAction) { break; } - action.to_publish.retry_count += 1; + action.retry_count += 1; tokio::time::sleep(Duration::from_millis(100)).await; - if action.to_publish.retry_count >= MAX_PUBLISH_RETRY { + if action.retry_count >= MAX_PUBLISH_RETRY { tracing::info!( - request_id = ?CryptoHash(action.to_publish.request_id), + sign_id = ?action.request.indexed.id, elapsed = ?action.timestamp.elapsed(), "exceeded max retries, trashing publish request", ); break; } else { tracing::info!( - request_id = ?CryptoHash(action.to_publish.request_id), - retry_count = action.to_publish.retry_count, + sign_id = ?action.request.indexed.id, + retry_count = action.retry_count, elapsed = ?action.timestamp.elapsed(), "failed to publish, retrying" ); @@ -406,24 +413,16 @@ async fn execute_publish(client: ChainClient, mut action: PublishAction) { async fn try_publish_near( near: &NearClient, - to_publish: &ToPublish, + action: &PublishAction, timestamp: &Instant, - signature: &SignatureResponse, + signature: &Signature, ) -> Result<(), near_fetch::Error> { - let ToPublish { - request_id, - request, - time_added, - .. - } = to_publish; - let outcome = near - .call_respond(request, signature) + .call_respond(&action.request.indexed.id, signature) .await .inspect_err(|err| { tracing::error!( - request_id = ?CryptoHash(*request_id), - ?request, + sign_id = ?action.request.indexed.id, ?err, "failed to publish signature", ); @@ -434,9 +433,8 @@ async fn try_publish_near( let _: () = outcome.json().inspect_err(|err| { tracing::error!( - request_id = ?CryptoHash(*request_id), - ?request, - big_r = signature.big_r.affine_point.to_base58(), + sign_id = ?action.request.indexed.id, + big_r = signature.big_r.to_base58(), s = ?signature.s, ?err, "smart contract threw error", @@ -446,9 +444,8 @@ async fn try_publish_near( .inc(); })?; tracing::info!( - request_id = ?CryptoHash(*request_id), - ?request, - big_r = signature.big_r.affine_point.to_base58(), + sign_id = ?action.request.indexed.id, + big_r = signature.big_r.to_base58(), s = ?signature.s, elapsed = ?timestamp.elapsed(), "published signature sucessfully", @@ -459,11 +456,11 @@ async fn try_publish_near( .inc(); crate::metrics::SIGN_TOTAL_LATENCY .with_label_values(&[near.my_account_id.as_str()]) - .observe(time_added.elapsed().as_secs_f64()); + .observe(action.request.indexed.timestamp.elapsed().as_secs_f64()); crate::metrics::SIGN_RESPOND_LATENCY .with_label_values(&[near.my_account_id.as_str()]) .observe(timestamp.elapsed().as_secs_f64()); - if time_added.elapsed().as_secs() <= 30 { + if action.request.indexed.timestamp.elapsed().as_secs() <= 30 { crate::metrics::NUM_SIGN_SUCCESS_30S .with_label_values(&[near.my_account_id.as_str()]) .inc(); @@ -474,26 +471,20 @@ async fn try_publish_near( async fn try_publish_eth( eth: &EthClient, - to_publish: &ToPublish, + action: &PublishAction, timestamp: &Instant, - signature: &SignatureResponse, + signature: &Signature, ) -> Result<(), ()> { - let ToPublish { request_id, .. } = to_publish; let params = [Token::Array(vec![Token::Tuple(vec![ - Token::FixedBytes(request_id.to_vec()), + Token::FixedBytes(action.request.indexed.id.request_id.to_vec()), Token::Tuple(vec![ Token::Tuple(vec![ - Token::Uint(U256::from_big_endian(&signature.big_r.affine_point.x())), + Token::Uint(U256::from_big_endian(&signature.big_r.x())), Token::Uint(U256::from_big_endian( - signature - .big_r - .affine_point - .to_encoded_point(false) - .y() - .unwrap(), + signature.big_r.to_encoded_point(false).y().unwrap(), )), ]), - Token::Uint(U256::from_big_endian(&signature.s.scalar.to_bytes())), + Token::Uint(U256::from_big_endian(&signature.s.to_bytes())), signature.recovery_id.into_token(), ]), ])])]; @@ -529,7 +520,7 @@ async fn try_publish_eth( match result { Ok(tx_hash) => { tracing::info!( - request_id = ?CryptoHash(*request_id), + sign_id = ?action.request.indexed.id, tx_hash = ?tx_hash, elapsed = ?timestamp.elapsed(), "published ethereum signature successfully" @@ -538,7 +529,7 @@ async fn try_publish_eth( } Err(err) => { tracing::error!( - request_id = ?CryptoHash(*request_id), + sign_id = ?action.request.indexed.id, error = ?err, "failed to publish ethereum signature" ); diff --git a/chain-signatures/primitives/Cargo.toml b/chain-signatures/primitives/Cargo.toml new file mode 100644 index 00000000..dcf7a4a8 --- /dev/null +++ b/chain-signatures/primitives/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mpc-primitives" +version.workspace = true +edition = "2021" + +[dependencies] +ciborium.workspace = true +hex.workspace = true +k256.workspace = true +near-account-id.workspace = true +near-sdk.workspace = true +serde.workspace = true +serde_bytes.workspace = true +sha3.workspace = true + +mpc-crypto.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2.12", features = ["custom"] } + +[dev-dependencies] +borsh.workspace = true +serde_json.workspace = true diff --git a/chain-signatures/primitives/src/bytes.rs b/chain-signatures/primitives/src/bytes.rs new file mode 100644 index 00000000..aba5f47a --- /dev/null +++ b/chain-signatures/primitives/src/bytes.rs @@ -0,0 +1,116 @@ +/// Scalar module for any scalars to be sent through messaging other nodes. +/// There's an issue with serializing with ciborium when it comes to +/// forward and backward compatibility, so we need to implement our own +/// custom serialization here. +pub mod cbor_scalar { + use k256::elliptic_curve::bigint::Encoding as _; + use k256::elliptic_curve::scalar::FromUintUnchecked as _; + use k256::Scalar; + use serde::{de, Deserialize as _, Deserializer, Serialize, Serializer}; + + pub fn serialize(scalar: &Scalar, ser: S) -> Result { + let num = k256::U256::from(scalar); + let bytes = num.to_le_bytes(); + serde_bytes::Bytes::new(&bytes).serialize(ser) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let bytes = match ciborium::Value::deserialize(deserializer)? { + ciborium::Value::Bytes(bytes) if bytes.len() != 32 => { + return Err(de::Error::custom("expected 32 bytes for Scalar")) + } + ciborium::Value::Bytes(bytes) => bytes, + _ => return Err(de::Error::custom("expected ciborium::Value::Bytes")), + }; + + let mut buf = [0u8; 32]; + buf.copy_from_slice(&bytes[0..32]); + + let num = k256::U256::from_le_bytes(buf); + let scalar = k256::Scalar::from_uint_unchecked(num); + Ok(scalar) + } +} + +pub mod borsh_scalar { + use k256::Scalar; + use mpc_crypto::ScalarExt as _; + use near_sdk::borsh::{BorshDeserialize, BorshSerialize}; + use std::io; + + pub fn serialize(scalar: &Scalar, writer: &mut W) -> io::Result<()> { + let to_ser: [u8; 32] = scalar.to_bytes().into(); + BorshSerialize::serialize(&to_ser, writer) + } + + pub fn deserialize_reader(reader: &mut R) -> io::Result { + let from_ser: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?; + let scalar = Scalar::from_bytes(from_ser).ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "Scalar bytes are not in the k256 field", + ))?; + Ok(scalar) + } +} + +pub mod borsh_affine_point { + use k256::AffinePoint; + use near_sdk::borsh::{BorshDeserialize, BorshSerialize}; + use near_sdk::serde_json; + use std::io; + use std::io::prelude::{Read, Write}; + + pub fn serialize(affine_point: &AffinePoint, writer: &mut W) -> io::Result<()> { + let to_ser: Vec = serde_json::to_vec(affine_point)?; + BorshSerialize::serialize(&to_ser, writer) + } + + pub fn deserialize_reader(reader: &mut R) -> io::Result { + let from_ser: Vec = BorshDeserialize::deserialize_reader(reader)?; + Ok(serde_json::from_slice(&from_ser)?) + } +} + +#[cfg(test)] +mod tests { + use borsh::{BorshDeserialize, BorshSerialize}; + use k256::{elliptic_curve::PrimeField, Scalar}; + use mpc_crypto::ScalarExt as _; + use serde::{Deserialize, Serialize}; + + #[test] + fn serializeable_scalar_roundtrip() { + let test_vec = vec![ + Scalar::ZERO, + Scalar::ONE, + Scalar::from_u128(u128::MAX), + Scalar::from_bytes([3; 32]).unwrap(), + ]; + + #[derive(Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)] + struct WithScalar { + #[borsh( + serialize_with = "super::borsh_scalar::serialize", + deserialize_with = "super::borsh_scalar::deserialize_reader" + )] + scalar: Scalar, + } + + for scalar in test_vec.into_iter() { + let input = WithScalar { scalar }; + // Test borsh + { + let serialized = borsh::to_vec(&input).unwrap(); + let output: WithScalar = borsh::from_slice(&serialized).unwrap(); + assert_eq!(input, output, "Failed on {:?}", scalar); + } + + // Test Serde via JSON + { + let serialized = serde_json::to_vec(&input).unwrap(); + let output: WithScalar = serde_json::from_slice(&serialized).unwrap(); + assert_eq!(input, output, "Failed on {:?}", scalar); + } + } + } +} diff --git a/chain-signatures/primitives/src/lib.rs b/chain-signatures/primitives/src/lib.rs new file mode 100644 index 00000000..8e6b84af --- /dev/null +++ b/chain-signatures/primitives/src/lib.rs @@ -0,0 +1,90 @@ +pub mod bytes; + +use k256::{AffinePoint, Scalar}; +use near_account_id::AccountId; +use near_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use sha3::Digest; + +use crate::bytes::cbor_scalar; + +#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "near_sdk::borsh")] +pub struct SignId { + #[serde(with = "serde_bytes")] + pub request_id: [u8; 32], +} + +impl std::fmt::Debug for SignId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SignId") + .field(&hex::encode(self.request_id)) + .finish() + } +} + +impl SignId { + pub fn new(request_id: [u8; 32]) -> Self { + Self { request_id } + } + + pub fn from_parts(id: &AccountId, payload: &[u8; 32], path: &str, key_version: u32) -> Self { + let mut hasher = sha3::Sha3_256::new(); + hasher.update(id.as_bytes()); + hasher.update(payload); + hasher.update(path.as_bytes()); + hasher.update(key_version.to_le_bytes()); + let request_id: [u8; 32] = hasher.finalize().into(); + Self { request_id } + } +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct SignArgs { + #[serde(with = "serde_bytes")] + pub entropy: [u8; 32], + #[serde(with = "cbor_scalar")] + pub epsilon: Scalar, + #[serde(with = "cbor_scalar")] + pub payload: Scalar, + pub path: String, + pub key_version: u32, +} + +impl std::fmt::Debug for SignArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SignArgs") + .field("entropy", &hex::encode(self.entropy)) + .field("epsilon", &self.epsilon) + .field("payload", &self.payload) + .field("path", &self.path) + .field("key_version", &self.key_version) + .finish() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "near_sdk::borsh")] +pub struct Signature { + #[borsh( + serialize_with = "bytes::borsh_affine_point::serialize", + deserialize_with = "bytes::borsh_affine_point::deserialize_reader" + )] + pub big_r: AffinePoint, + #[borsh( + serialize_with = "bytes::borsh_scalar::serialize", + deserialize_with = "bytes::borsh_scalar::deserialize_reader" + )] + pub s: Scalar, + pub recovery_id: u8, +} + +impl Signature { + pub fn new(big_r: AffinePoint, s: Scalar, recovery_id: u8) -> Self { + Signature { + big_r, + s, + recovery_id, + } + } +} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 47c61119..44337b76 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -50,6 +50,7 @@ mpc-crypto.workspace = true mpc-contract.workspace = true mpc-keys.workspace = true mpc-node.workspace = true +mpc-primitives.workspace = true [dev-dependencies] test-log = { version = "0.2.12", features = ["log", "trace"] } diff --git a/integration-tests/src/actions/sign.rs b/integration-tests/src/actions/sign.rs index ea603f51..a056e875 100644 --- a/integration-tests/src/actions/sign.rs +++ b/integration-tests/src/actions/sign.rs @@ -2,13 +2,10 @@ use std::fmt; use std::future::IntoFuture; use cait_sith::FullSignature; -use k256::{Scalar, Secp256k1}; +use k256::Secp256k1; use mpc_contract::errors; -use mpc_contract::primitives::{SignRequest, SignatureRequest}; -use mpc_crypto::{ - derive_epsilon_near, ScalarExt as _, SerializableAffinePoint, SerializableScalar, - SignatureResponse, -}; +use mpc_contract::primitives::SignRequest; +use mpc_primitives::{SignId, Signature}; use near_crypto::InMemorySigner; use near_fetch::ops::AsyncTransactionStatus; use near_workspaces::types::{Gas, NearToken}; @@ -240,12 +237,6 @@ impl SignAction<'_> { public_key: rogue.secret_key().public_key().clone().into(), secret_key: rogue.secret_key().to_string().parse()?, }; - let epsilon = derive_epsilon_near(predecessor, &self.path); - - let request = SignatureRequest { - payload_hash: Scalar::from_bytes(payload_hash).unwrap().into(), - epsilon: SerializableScalar { scalar: epsilon }, - }; let big_r = serde_json::from_value( "02EC7FA686BB430A4B700BDA07F2E07D6333D9E33AEEF270334EB2D00D0A6FEC6C".into(), @@ -254,21 +245,20 @@ impl SignAction<'_> { "20F90C540EE00133C911EA2A9ADE2ABBCC7AD820687F75E011DFEEC94DB10CD6".into(), )?; // Fake S - let response = SignatureResponse { - big_r: SerializableAffinePoint { - affine_point: big_r, - }, - s: SerializableScalar { scalar: s }, + let signature = Signature { + big_r, + s, recovery_id: 0, }; + let sign_id = SignId::from_parts(predecessor, &payload_hash, &self.path, self.key_version); let status = self .nodes .rpc_client .call(&signer, self.nodes.contract().id(), "respond") .args_json(serde_json::json!({ - "request": request, - "response": response, + "sign_id": sign_id, + "signature": signature, })) .max_gas() .transact_async() diff --git a/integration-tests/src/actions/wait_for.rs b/integration-tests/src/actions/wait_for.rs index d039c930..f6ccf251 100644 --- a/integration-tests/src/actions/wait_for.rs +++ b/integration-tests/src/actions/wait_for.rs @@ -6,7 +6,7 @@ use backon::ConstantBuilder; use backon::Retryable; use cait_sith::FullSignature; use k256::Secp256k1; -use mpc_crypto::SignatureResponse; +use mpc_primitives::Signature; use near_fetch::ops::AsyncTransactionStatus; use near_primitives::errors::ActionErrorKind; use near_primitives::hash::CryptoHash; @@ -57,12 +57,12 @@ pub async fn signature_responded( return Ok(Outcome::Failed(format!("{:?}", outcome.status()))); } - let result: SignatureResponse = outcome + let result: Signature = outcome .json() .map_err(|err| WaitForError::SerdeJson(format!("{err:?}")))?; Ok(Outcome::Signature(cait_sith::FullSignature:: { - big_r: result.big_r.affine_point, - s: result.s.scalar, + big_r: result.big_r, + s: result.s, })) }; @@ -187,11 +187,11 @@ pub async fn batch_signature_responded( { match receipt_outcome.status { ExecutionStatusView::SuccessValue(value) => { - let result: SignatureResponse = serde_json::from_slice(&value) + let result: Signature = serde_json::from_slice(&value) .map_err(|err| WaitForError::SerdeJson(format!("{err:?}")))?; let signature = cait_sith::FullSignature:: { - big_r: result.big_r.affine_point, - s: result.s.scalar, + big_r: result.big_r, + s: result.s, }; signatures.push(signature); } diff --git a/integration-tests/src/commands.rs b/integration-tests/src/commands.rs index fdb59fad..c952dfd2 100644 --- a/integration-tests/src/commands.rs +++ b/integration-tests/src/commands.rs @@ -1,13 +1,12 @@ use std::str::FromStr; -use k256::Scalar; use mpc_contract::{ config::Config, - primitives::{CandidateInfo, Candidates, Participants, SignRequest, SignatureRequest}, + primitives::{CandidateInfo, Candidates, Participants, SignRequest}, update::ProposeUpdateArgs, }; -use mpc_crypto::{ScalarExt, SerializableAffinePoint, SerializableScalar, SignatureResponse}; use mpc_keys::hpke; +use mpc_primitives::{SignId, Signature}; use near_account_id::AccountId; use near_primitives::borsh; use near_sdk::PublicKey; @@ -20,7 +19,7 @@ const PAYLOAD: [u8; 32] = [ const SIGN_PK: &str = "ed25519:J75xXmF7WUPS3xCm3hy2tgwLCKdYM1iJd4BWF8sWVnae"; -pub fn sing_command(contract_id: &AccountId, caller_id: &AccountId) -> anyhow::Result { +pub fn sign_command(contract_id: &AccountId, caller_id: &AccountId) -> anyhow::Result { let sign_request = SignRequest { payload: PAYLOAD, path: "test".into(), @@ -40,14 +39,10 @@ pub fn sing_command(contract_id: &AccountId, caller_id: &AccountId) -> anyhow::R pub fn respond_command(contract_id: &AccountId, caller_id: &AccountId) -> anyhow::Result { let payload_hashed = web3::signing::keccak256(&PAYLOAD); + let path = "test"; + let key_version = 0; - let request = SignatureRequest::new( - Scalar::from_bytes(payload_hashed) - .ok_or_else(|| anyhow::anyhow!("Failed to convert bytes to Scalar"))?, - caller_id, - "test", - ); - + let sign_id = SignId::from_parts(caller_id, &payload_hashed, path, key_version); let big_r = serde_json::from_value( "02EC7FA686BB430A4B700BDA07F2E07D6333D9E33AEEF270334EB2D00D0A6FEC6C".into(), )?; // Fake BigR @@ -55,17 +50,15 @@ pub fn respond_command(contract_id: &AccountId, caller_id: &AccountId) -> anyhow "20F90C540EE00133C911EA2A9ADE2ABBCC7AD820687F75E011DFEEC94DB10CD6".into(), )?; // Fake S - let response = SignatureResponse { - big_r: SerializableAffinePoint { - affine_point: big_r, - }, - s: SerializableScalar { scalar: s }, + let signature = Signature { + big_r, + s, recovery_id: 0, }; let request_json = format!( "'{}'", - serde_json::to_string(&json!({"request": request, "response": response})).unwrap() + serde_json::to_string(&json!({"sign_id": sign_id, "signature": signature})).unwrap() ); Ok(format!( diff --git a/integration-tests/src/main.rs b/integration-tests/src/main.rs index 1f0c794d..e3769746 100644 --- a/integration-tests/src/main.rs +++ b/integration-tests/src/main.rs @@ -132,7 +132,7 @@ async fn main() -> anyhow::Result<()> { .to_string() ); - doc.push(commands::sing_command( + doc.push(commands::sign_command( &contract_account_id, &caller_account_id, )?); diff --git a/integration-tests/tests/cases/mod.rs b/integration-tests/tests/cases/mod.rs index 5a261ad0..1688856f 100644 --- a/integration-tests/tests/cases/mod.rs +++ b/integration-tests/tests/cases/mod.rs @@ -123,12 +123,12 @@ async fn test_key_derivation() -> anyhow::Result<()> { let user_secp_pk = secp256k1::PublicKey::from_x_only_public_key(user_pk_x, user_pk_y_parity); let user_addr = actions::public_key_to_address(&user_secp_pk); - let r = x_coordinate(&multichain_sig.big_r.affine_point); + let r = x_coordinate(&multichain_sig.big_r); let s = multichain_sig.s; let signature_for_recovery: [u8; 64] = { let mut signature = [0u8; 64]; signature[..32].copy_from_slice(&r.to_bytes()); - signature[32..].copy_from_slice(&s.scalar.to_bytes()); + signature[32..].copy_from_slice(&s.to_bytes()); signature }; let recovered_addr = web3::signing::recover(