diff --git a/Cargo.lock b/Cargo.lock index c616ec0c8..3739169c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5288,6 +5288,7 @@ dependencies = [ "mpc-primitives", "near-abi", "near-account-id 2.5.0", + "near-mpc-sdk", "near-sdk", "near-workspaces", "rand 0.8.5", @@ -6243,6 +6244,14 @@ dependencies = [ "serde_json", ] +[[package]] +name = "near-mpc-sdk" +version = "3.5.1" +dependencies = [ + "bounded-collections", + "contract-interface", +] + [[package]] name = "near-network" version = "2.10.6" diff --git a/Cargo.toml b/Cargo.toml index 2153e8c2e..485fc458f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/foreign-chain-rpc-interfaces", "crates/include-measurements", "crates/mpc-attestation", + "crates/near-mpc-sdk", "crates/node", "crates/node-types", "crates/primitives", @@ -44,6 +45,7 @@ mpc-contract = { path = "crates/contract", features = ["dev-utils"] } mpc-node = { path = "crates/node" } mpc-primitives = { path = "crates/primitives", features = ["abi"] } mpc-tls = { path = "crates/tls" } +near-mpc-sdk = { path = "crates/near-mpc-sdk" } node-types = { path = "crates/node-types" } tee-authority = { path = "crates/tee-authority" } test-utils = { path = "crates/test-utils" } diff --git a/crates/contract-interface/src/lib.rs b/crates/contract-interface/src/lib.rs index b04e50f89..9d34e800c 100644 --- a/crates/contract-interface/src/lib.rs +++ b/crates/contract-interface/src/lib.rs @@ -15,9 +15,10 @@ pub mod types { pub use metrics::Metrics; pub use primitives::{ - AccountId, CkdAppId, DomainId, K256AffinePoint, K256Scalar, K256Signature, - SignatureResponse, Tweak, + AccountId, CkdAppId, DomainId, Ed25519Signature, K256AffinePoint, K256Scalar, + K256Signature, SignatureResponse, Tweak, }; + pub use sign::*; pub use state::{ AddDomainsVotes, AttemptId, AuthenticatedAccountId, AuthenticatedParticipantId, DomainConfig, DomainPurpose, DomainRegistry, EpochId, InitializingContractState, KeyEvent, @@ -34,6 +35,7 @@ pub mod types { mod metrics; mod participants; mod primitives; + mod sign; mod state; mod updates; } diff --git a/crates/contract-interface/src/types/primitives.rs b/crates/contract-interface/src/types/primitives.rs index 60cca5776..7fdce8871 100644 --- a/crates/contract-interface/src/types/primitives.rs +++ b/crates/contract-interface/src/types/primitives.rs @@ -102,7 +102,9 @@ pub enum SignatureResponse { } #[serde_as] -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +#[derive( + Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, derive_more::From, +)] #[cfg_attr( all(feature = "abi", not(target_arch = "wasm32")), derive(schemars::JsonSchema) diff --git a/crates/contract-interface/src/types/sign.rs b/crates/contract-interface/src/types/sign.rs new file mode 100644 index 000000000..a35fb9c7a --- /dev/null +++ b/crates/contract-interface/src/types/sign.rs @@ -0,0 +1,31 @@ +use bounded_collections::{BoundedVec, hex_serde}; +use serde::{Deserialize, Serialize}; + +use crate::types::DomainId; + +pub const ECDSA_PAYLOAD_SIZE_BYTES: usize = 32; + +pub const EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES: usize = 32; +// Transaction signatures for Solana is over the whole transaction payload, +// not the transaction hash. The max size for a solana transaction is 1232 bytes, +// to fit in a single UDP packet, hence the 1232 byte upper bounds. +pub const EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES: usize = 1232; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct SignRequestArgs { + pub path: String, + pub payload_v2: Payload, + pub domain_id: DomainId, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum Payload { + Ecdsa( + #[serde(with = "hex_serde")] + BoundedVec, + ), + Eddsa( + #[serde(with = "hex_serde")] + BoundedVec, + ), +} diff --git a/crates/contract/Cargo.toml b/crates/contract/Cargo.toml index 8cd225251..1ab3ad68a 100644 --- a/crates/contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -93,6 +93,7 @@ futures = { workspace = true } insta = { workspace = true } mpc-contract = { workspace = true, features = ["test-utils"] } near-abi = { workspace = true } +near-mpc-sdk = { workspace = true } near-workspaces = { workspace = true } rand = { workspace = true } rand_core = { workspace = true } diff --git a/crates/contract/src/primitives/signature.rs b/crates/contract/src/primitives/signature.rs index 1e588d9b4..11c461add 100644 --- a/crates/contract/src/primitives/signature.rs +++ b/crates/contract/src/primitives/signature.rs @@ -1,6 +1,10 @@ use crate::crypto_shared; use crate::errors::{Error, InvalidParameters}; use crate::DomainId; +use contract_interface::types::{ + ECDSA_PAYLOAD_SIZE_BYTES, EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES, + EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES, +}; use crypto_shared::derive_tweak; use near_account_id::AccountId; use near_sdk::{near, CryptoHash}; @@ -34,7 +38,7 @@ pub enum Payload { definitions = "<[u8; 32] as ::borsh::BorshSchema>::add_definitions_recursively" ),)) )] - Bytes<32, 32>, + Bytes, ), Eddsa( #[cfg_attr( @@ -45,7 +49,7 @@ pub enum Payload { definitions = " as ::borsh::BorshSchema>::add_definitions_recursively" ),)) )] - Bytes<32, 1232>, + Bytes, ), } diff --git a/crates/contract/tests/sandbox/upgrade_to_current_contract.rs b/crates/contract/tests/sandbox/upgrade_to_current_contract.rs index c37b1ec01..ffded7a82 100644 --- a/crates/contract/tests/sandbox/upgrade_to_current_contract.rs +++ b/crates/contract/tests/sandbox/upgrade_to_current_contract.rs @@ -15,7 +15,6 @@ use contract_interface::method_names; use contract_interface::types::{self as dtos, ProtocolContractState}; use mpc_contract::{ crypto_shared::CKDResponse, - crypto_shared::SignatureResponse, primitives::{ domain::{DomainConfig, DomainPurpose, SignatureScheme}, key_state::{EpochId, Keyset}, @@ -24,6 +23,7 @@ use mpc_contract::{ }, }; use near_account_id::AccountId; +use near_mpc_sdk::sign::SignatureRequestResponse; use near_workspaces::{network::Sandbox, Account, Contract, Worker}; use rand_core::OsRng; use rstest::rstest; @@ -287,7 +287,7 @@ async fn upgrade_preserves_state_and_requests( .unwrap(); let execution = pending.transaction.await.unwrap().into_result().unwrap(); - let returned: SignatureResponse = execution.json().unwrap(); + let returned: SignatureRequestResponse = execution.json().unwrap(); assert_eq!( returned, pending.response.response, @@ -434,7 +434,7 @@ async fn upgrade_allows_new_request_types( .unwrap(); let execution = pending.transaction.await.unwrap().into_result().unwrap(); - let returned: SignatureResponse = execution.json().unwrap(); + let returned: SignatureRequestResponse = execution.json().unwrap(); assert_eq!( returned, pending.response.response, diff --git a/crates/contract/tests/sandbox/utils/sign_utils.rs b/crates/contract/tests/sandbox/utils/sign_utils.rs index 7e43f633a..687a55db1 100644 --- a/crates/contract/tests/sandbox/utils/sign_utils.rs +++ b/crates/contract/tests/sandbox/utils/sign_utils.rs @@ -11,24 +11,26 @@ use contract_interface::method_names::{ use contract_interface::types::{self as dtos}; use digest::{Digest, FixedOutput}; use ecdsa::signature::Verifier as _; +use k256::elliptic_curve::sec1::ToEncodedPoint as _; use k256::{ elliptic_curve::{point::DecompressPoint as _, Field as _, Group as _}, AffinePoint, FieldBytes, Secp256k1, }; use mpc_contract::{ crypto_shared::{ - derive_key_secp256k1, derive_tweak, ed25519_types, k256_types, - k256_types::SerializableAffinePoint, kdf::check_ec_signature, kdf::derive_app_id, - CKDResponse, SerializableScalar, SignatureResponse, + derive_key_secp256k1, derive_tweak, kdf::check_ec_signature, kdf::derive_app_id, + CKDResponse, }, errors, primitives::{ ckd::{CKDRequest, CKDRequestArgs}, domain::DomainId, - signature::{Bytes, Payload, SignRequestArgs, SignatureRequest, YieldIndex}, + signature::{Bytes, Payload, SignatureRequest, YieldIndex}, }, }; use near_account_id::AccountId; +use near_mpc_sdk::sign::{Ed25519Signature, K256AffinePoint, K256Scalar, K256Signature}; +use near_mpc_sdk::sign::{SignRequestArgs, SignRequestBuilder, SignatureRequestResponse}; use near_workspaces::{ network::Sandbox, operations::TransactionStatus, types::NearToken, Account, Contract, Worker, }; @@ -156,7 +158,7 @@ impl SignRequestTest { let execution = status.await?; dbg!(&execution); let execution = execution.into_result()?; - let returned_resp: SignatureResponse = execution.json()?; + let returned_resp: SignatureRequestResponse = execution.json()?; assert_eq!( returned_resp, self.response.response, "Returned signature request does not match" @@ -176,7 +178,7 @@ impl SignRequestTest { #[derive(Debug, Serialize)] pub struct SignResponseArgs { pub request: SignatureRequest, - pub response: SignatureResponse, + pub response: SignatureRequestResponse, } impl SignResponseArgs { @@ -458,7 +460,7 @@ fn create_response_secp256k1( msg: &str, path: &str, signing_key: &ts_ecdsa::KeygenOutput, -) -> (Payload, SignatureRequest, SignatureResponse) { +) -> (Payload, SignatureRequest, SignatureRequestResponse) { let (digest, payload) = process_message(msg); let pk = signing_key.public_key; let tweak = derive_tweak(predecessor_id, path); @@ -478,7 +480,6 @@ fn create_response_secp256k1( let respond_req = SignatureRequest::new(domain_id, payload.clone(), predecessor_id, path); let big_r = AffinePoint::decompress(&r_bytes, k256::elliptic_curve::subtle::Choice::from(0)).unwrap(); - let s: k256::Scalar = *s.as_ref(); let recovery_id = if check_ec_signature(&derived_pk, &big_r, &s, payload.as_ecdsa().unwrap(), 0) .is_ok() @@ -490,11 +491,15 @@ fn create_response_secp256k1( panic!("unable to use recovery id of 0 or 1"); }; - let respond_resp = SignatureResponse::Secp256k1(k256_types::Signature { - big_r: SerializableAffinePoint { - affine_point: big_r, + let encoded_point = big_r.to_encoded_point(true); + + let respond_resp = SignatureRequestResponse::Secp256k1(K256Signature { + big_r: K256AffinePoint { + affine_point: encoded_point.as_bytes().try_into().unwrap(), + }, + s: K256Scalar { + scalar: s.to_bytes().into(), }, - s: SerializableScalar { scalar: s }, recovery_id, }); @@ -507,7 +512,7 @@ fn create_response_ed25519( msg: &str, path: &str, signing_key: &eddsa::KeygenOutput, -) -> (Payload, SignatureRequest, SignatureResponse) { +) -> (Payload, SignatureRequest, SignatureRequestResponse) { let tweak = derive_tweak(predecessor_id, path); let derived_signing_key = derive_secret_key_ed25519(signing_key, &tweak); @@ -521,7 +526,7 @@ fn create_response_ed25519( frost_ed25519::SigningKey::from_scalar(derived_signing_key.private_share.to_scalar()) .unwrap(); - let signature = derived_signing_key + let signature: [u8; 64] = derived_signing_key .sign(OsRng, &payload) .serialize() .unwrap() @@ -533,8 +538,8 @@ fn create_response_ed25519( let respond_req = SignatureRequest::new(domain_id, payload.clone(), predecessor_id, path); - let signature_response = SignatureResponse::Ed25519 { - signature: ed25519_types::Signature::new(signature), + let signature_response = SignatureRequestResponse::Ed25519 { + signature: Ed25519Signature::from(signature), }; (payload, respond_req, signature_response) @@ -570,12 +575,13 @@ fn gen_ed25519_sign_test( let path: String = rng.gen::().to_string(); let (payload, request, response) = create_response_ed25519(domain_id, predecessor_id, &msg, &path, sk); - let args = SignRequestArgs { - payload_v2: Some(payload.clone()), - path, - domain_id: Some(domain_id), - ..Default::default() - }; + let args = SignRequestBuilder::new() + .with_path(path) + .with_payload(near_mpc_sdk::sign::Payload::Eddsa( + payload.as_eddsa().unwrap().to_vec().try_into().unwrap(), + )) + .with_domain_id(domain_id.0) + .build(); SignRequestTest { response: SignResponseArgs { request, response }, args, @@ -592,12 +598,14 @@ pub fn gen_secp_256k1_sign_test( let path: String = rng.gen::().to_string(); let (payload, request, response) = create_response_secp256k1(domain_id, predecessor_id, &msg, &path, sk); - let args = SignRequestArgs { - payload_v2: Some(payload.clone()), - path, - domain_id: Some(domain_id), - ..Default::default() - }; + + let payload_bytes: [u8; 32] = *payload.as_ecdsa().unwrap(); + + let args = SignRequestBuilder::new() + .with_path(path) + .with_payload(near_mpc_sdk::sign::Payload::Ecdsa(payload_bytes.into())) + .with_domain_id(domain_id.0) + .build(); SignRequestTest { response: SignResponseArgs { request, response }, args, diff --git a/crates/devnet/src/contracts.rs b/crates/devnet/src/contracts.rs index 9e11b6537..509de749e 100644 --- a/crates/devnet/src/contracts.rs +++ b/crates/devnet/src/contracts.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; -use contract_interface::method_names; +use contract_interface::{ + method_names, + types::{EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES, EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES}, +}; use mpc_contract::primitives::{ ckd::CKDRequestArgs, domain::{DomainConfig, SignatureScheme}, @@ -168,7 +171,9 @@ fn make_payload(scheme: SignatureScheme) -> Payload { Payload::Ecdsa(Bytes::new(rand::random::<[u8; 32]>().to_vec()).unwrap()) } SignatureScheme::Ed25519 => { - let len = rand::random_range(32..=1232); + let len = rand::random_range( + EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES..=EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES, + ); let mut payload = vec![0; len]; rand::rng().fill_bytes(&mut payload); Payload::Eddsa(Bytes::new(payload).unwrap()) diff --git a/crates/near-mpc-sdk/Cargo.toml b/crates/near-mpc-sdk/Cargo.toml new file mode 100644 index 000000000..2c4b6b2e1 --- /dev/null +++ b/crates/near-mpc-sdk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "near-mpc-sdk" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bounded-collections = { workspace = true } +contract-interface = { workspace = true } + +[dev-dependencies] + +[lints] +workspace = true diff --git a/crates/near-mpc-sdk/src/lib.rs b/crates/near-mpc-sdk/src/lib.rs new file mode 100644 index 000000000..b61f78ce0 --- /dev/null +++ b/crates/near-mpc-sdk/src/lib.rs @@ -0,0 +1,4 @@ +pub use bounded_collections; +pub use contract_interface; + +pub mod sign; diff --git a/crates/near-mpc-sdk/src/sign.rs b/crates/near-mpc-sdk/src/sign.rs new file mode 100644 index 000000000..4879e8dd5 --- /dev/null +++ b/crates/near-mpc-sdk/src/sign.rs @@ -0,0 +1,156 @@ +pub use contract_interface::method_names::SIGN as SIGN_METHOD_NAME; +use contract_interface::types::DomainId; +// response types +pub use contract_interface::types::{ + Ed25519Signature, K256AffinePoint, K256Scalar, K256Signature, + SignatureResponse as SignatureRequestResponse, +}; + +// raw request arg type +pub use contract_interface::types::{Payload, SignRequestArgs}; + +#[derive(Debug, Clone)] +pub struct NotSet; + +#[derive(Debug, Clone)] +pub struct SignRequestBuilder { + path: Path, + payload: Payload, + domain_id: DomainId, +} + +impl Default for SignRequestBuilder { + fn default() -> Self { + Self::new() + } +} + +impl SignRequestBuilder { + pub fn new() -> Self { + Self { + path: NotSet, + payload: NotSet, + domain_id: NotSet, + } + } + + pub fn with_path(self, path: String) -> SignRequestBuilder { + SignRequestBuilder { + path, + payload: NotSet, + domain_id: NotSet, + } + } +} + +impl SignRequestBuilder { + pub fn with_payload( + self, + payload: impl Into, + ) -> SignRequestBuilder { + SignRequestBuilder { + path: self.path, + payload: payload.into(), + domain_id: NotSet, + } + } +} + +impl SignRequestBuilder { + pub fn with_domain_id( + self, + domain_id: impl Into, + ) -> SignRequestBuilder { + SignRequestBuilder { + path: self.path, + payload: self.payload, + domain_id: domain_id.into(), + } + } +} + +impl SignRequestBuilder { + pub fn build(self) -> SignRequestArgs { + SignRequestArgs { + path: self.path, + payload_v2: self.payload, + domain_id: self.domain_id, + } + } +} + +#[cfg(test)] +mod test { + use bounded_collections::BoundedVec; + + use super::*; + + #[test] + fn builder_builds_as_expected() { + // given + let path = "test_path".to_string(); + let payload = Payload::Ecdsa(BoundedVec::from([1_u8; 32])); + let domain_id = DomainId(2); + + // when + let built_sign_request_args = SignRequestBuilder::new() + .with_path(path.clone()) + .with_payload(payload.clone()) + .with_domain_id(domain_id) + .build(); + + // then + let expected = SignRequestArgs { + path, + payload_v2: payload, + domain_id, + }; + + assert_eq!(built_sign_request_args, expected); + } + + #[test] + fn with_path_sets_expected_value() { + // given + let path = "test_path".to_string(); + + // when + let builder = SignRequestBuilder::new().with_path(path.clone()); + + // then + assert_eq!(builder.path, path); + } + + #[test] + fn with_payload_sets_expected_value() { + // given + let path = "test_path".to_string(); + let payload = Payload::Ecdsa(BoundedVec::from([1_u8; 32])); + + let builder = SignRequestBuilder::new().with_path(path); + + // when + let builder = builder.with_payload(payload.clone()); + + // then + assert_eq!(builder.payload, payload); + } + + #[test] + fn with_domain_id_sets_expected_value() { + // given + let path = "test_path".to_string(); + let payload = Payload::Ecdsa(BoundedVec::from([1_u8; 32])); + let domain_id = 420; + + let builder = SignRequestBuilder::new() + .with_path(path) + .with_payload(payload); + + // when + let builder = builder.with_domain_id(domain_id); + + // then + assert_eq!(builder.domain_id, DomainId::from(domain_id)); + } +} diff --git a/crates/node/src/tests.rs b/crates/node/src/tests.rs index fc962cb94..a839c0a1b 100644 --- a/crates/node/src/tests.rs +++ b/crates/node/src/tests.rs @@ -1,7 +1,8 @@ use aes_gcm::{Aes256Gcm, KeyInit}; use contract_interface::types::{ BitcoinExtractor, BitcoinRpcRequest, ForeignChainRpcRequest, - VerifyForeignTransactionRequestArgs, + VerifyForeignTransactionRequestArgs, EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES, + EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES, }; use mpc_contract::primitives::key_state::Keyset; use mpc_contract::state::ProtocolContractState; @@ -277,7 +278,9 @@ pub async fn request_signature_and_await_response( Payload::Ecdsa(Bytes::new(payload.to_vec()).unwrap()) } SignatureScheme::Ed25519 => { - let len = rand::thread_rng().gen_range(32..1232); + let len = rand::thread_rng().gen_range( + EDDSA_PAYLOAD_SIZE_LOWER_BOUND_BYTES..EDDSA_PAYLOAD_SIZE_UPPER_BOUND_BYTES, + ); let mut payload = vec![0; len]; rand::thread_rng().fill_bytes(payload.as_mut()); Payload::Eddsa(Bytes::new(payload.to_vec()).unwrap())