diff --git a/crates/e2e-tests/README.md b/crates/e2e-tests/README.md index ab6eac0c79..a17c4f5846 100644 --- a/crates/e2e-tests/README.md +++ b/crates/e2e-tests/README.md @@ -123,13 +123,14 @@ impl NearBlockchain { ``` `DeployedContract` wraps the contract's account ID plus its own `near-kit` -client. It exposes `call` (from the contract account), `call_from`/ -`call_from_with_deposit` (from an arbitrary `NearKitCaller`), `view`, and -`state()` (parsed `ProtocolContractState`). +client. It exposes `call` (from the contract account), +`contract_handle` (a typed `MpcContractHandle` calling as a given +`NearKitCaller`), `view`, and `state()` (parsed `ProtocolContractState`). `NearKitCaller` binds a signer to a non-contract account (nodes voting, users submitting sign requests) and implements the `CallContract` transport trait, -so typed calls can go through `MpcContractHandle`. +so typed calls go through `MpcContractHandle` — the single source of each +method's wire format, gas, and deposit. ### 3. `MpcNode` / `MpcNodeSetup` — node process manager diff --git a/crates/e2e-tests/src/blockchain.rs b/crates/e2e-tests/src/blockchain.rs index 39868b38b7..43b6da71fa 100644 --- a/crates/e2e-tests/src/blockchain.rs +++ b/crates/e2e-tests/src/blockchain.rs @@ -155,75 +155,6 @@ impl DeployedContract { .map_err(|e| anyhow::anyhow!("contract call `{method}` failed: {e}")) } - pub async fn call_from( - &self, - client: &NearKitCaller, - method: &str, - args: serde_json::Value, - ) -> anyhow::Result { - client - .inner - .call(&self.contract_id, method) - .args(args) - .gas(MAX_GAS) - .send() - .await - .map_err(|e| anyhow::anyhow!("contract call `{method}` (external signer) failed: {e}")) - } - - pub async fn call_from_with_deposit( - &self, - client: &NearKitCaller, - method: &str, - args: serde_json::Value, - gas: near_kit::Gas, - deposit: near_kit::NearToken, - ) -> anyhow::Result { - client - .inner - .call(&self.contract_id, method) - .args(args) - .gas(gas) - .deposit(deposit) - .send() - .await - .map_err(|e| anyhow::anyhow!("contract call `{method}` (with deposit) failed: {e}")) - } - - /// Like [`Self::call_from`], but with an attached `deposit`. - pub async fn call_from_deposit( - &self, - client: &NearKitCaller, - method: &str, - args: serde_json::Value, - deposit: near_kit::NearToken, - ) -> anyhow::Result { - self.call_from_with_deposit(client, method, args, MAX_GAS, deposit) - .await - } - - /// Call a method whose arguments are borsh-serialized (e.g. `propose_update`). - pub async fn call_from_borsh_with_deposit( - &self, - client: &NearKitCaller, - method: &str, - args: A, - gas: near_kit::Gas, - deposit: near_kit::NearToken, - ) -> anyhow::Result { - client - .inner - .call(&self.contract_id, method) - .args_borsh(args) - .gas(gas) - .deposit(deposit) - .send() - .await - .map_err(|e| { - anyhow::anyhow!("contract call `{method}` (borsh args, with deposit) failed: {e}") - }) - } - pub async fn view( &self, method: &str, diff --git a/crates/e2e-tests/src/cluster.rs b/crates/e2e-tests/src/cluster.rs index 73a69db54a..b67cb32264 100644 --- a/crates/e2e-tests/src/cluster.rs +++ b/crates/e2e-tests/src/cluster.rs @@ -12,12 +12,13 @@ use near_mpc_contract_interface::{ client::MpcContractHandle, method_names, types::{ - AccountId as ContractAccountId, Attestation, AuthScheme, CKDAppPublicKey, ChainEntry, - ChainRouting, DomainConfig, DomainId, DomainPurpose, Ed25519PublicKey, EpochId, - ForeignChain, GovernanceThreshold, GovernanceThresholdParameters, MockAttestation, - ParticipantId, ParticipantInfo, Participants, Payload, ProposeUpdateArgs, - ProposedGovernanceThresholdParameters, Protocol, ProtocolContractState, ProviderConfig, - ProviderId, ReconstructionThreshold, SignRequestArgs, + AccountId as ContractAccountId, Attestation, AuthScheme, BackupServiceInfo, + CKDAppPublicKey, ChainEntry, ChainRouting, DestinationNodeInfo, DomainConfig, DomainId, + DomainPurpose, Ed25519PublicKey, EpochId, ForeignChain, GovernanceThreshold, + GovernanceThresholdParameters, MockAttestation, ParticipantId, ParticipantInfo, + Participants, Payload, ProposeUpdateArgs, ProposedGovernanceThresholdParameters, Protocol, + ProtocolContractState, ProviderConfig, ProviderId, ReconstructionThreshold, + SignRequestArgs, }, }; use rand::SeedableRng; @@ -48,7 +49,6 @@ pub fn cluster_poll_retry() -> ConstantBuilder { ) } -const NODE_MANAGEMENT_DEPOSIT: near_kit::NearToken = near_kit::NearToken::from_yoctonear(1); // The contract's default `key_event_timeout_blocks = 30` is ~18 s on // mainnet (~600 ms blocks). The e2e sandbox runs ~8 blocks/s, so the // same 30 collapses to ~3.7 s — too tight for the resharing @@ -57,7 +57,6 @@ const NODE_MANAGEMENT_DEPOSIT: near_kit::NearToken = near_kit::NearToken::from_y // load. Override to 240 blocks (~30 s in sandbox) as a comfortable // budget over mainnet's effective headroom. const KEY_EVENT_TIMEOUT_BLOCKS: u64 = 240; -const VOTE_FOREIGN_CHAIN_GAS: near_kit::Gas = near_kit::Gas::from_tgas(30); const CONTRACT_DEPLOY_TIMEOUT: Duration = Duration::from_secs(15); const PROPOSER_NODE_INDEX: usize = 0; @@ -520,9 +519,34 @@ impl MpcCluster { /// `Initializing` state. Does NOT wait for key generation to complete — /// use `add_domains_and_wait` for the full flow. pub async fn start_add_domains(&self, domains: Vec) -> anyhow::Result<()> { - let args = json!({ "domains": &domains }); - self.call_from_all_nodes_concurrently(method_names::VOTE_ADD_DOMAINS, args) - .await?; + let handles: Vec<_> = self + .nodes + .iter() + .zip(self.node_keys.iter()) + .enumerate() + .filter(|(_, (node, _))| matches!(node, MpcNodeState::Running(_))) + .map(|(i, (node, key))| { + let client = self + .blockchain + .client_for(node.account_id().as_ref(), key)?; + Ok(( + i, + node.account_id().clone(), + self.contract.handle_for(client), + )) + }) + .collect::>>()?; + + let votes = handles.iter().map(|(i, account, contract_handle)| { + let domains = domains.clone(); + async move { + contract_handle + .vote_add_domains(domains) + .await + .with_context(|| format!("node {i} ({account}) failed to vote_add_domains")) + } + }); + futures::future::try_join_all(votes).await?; self.wait_for_state( |s| matches!(s, ProtocolContractState::Initializing(_)), @@ -539,13 +563,9 @@ impl MpcCluster { node_index: usize, next_domain_id: u64, ) -> anyhow::Result { - let client = self.operator_client_for(node_index)?; self.contract - .call_from( - &client, - method_names::VOTE_CANCEL_KEYGEN, - json!({ "next_domain_id": next_domain_id }), - ) + .handle_for(self.operator_client_for(node_index)?) + .vote_cancel_keygen(next_domain_id) .await .with_context(|| format!("node {node_index} failed to send cancel keygen vote")) } @@ -582,8 +602,8 @@ impl MpcCluster { }; tracing::info!(?prospective_epoch_id, new_threshold, "voting for resharing"); - let args = json!({ "prospective_epoch_id": prospective_epoch_id, "proposal": proposal }); - self.vote_resharing(current_participants, args).await?; + self.vote_resharing(current_participants, prospective_epoch_id, proposal) + .await?; self.wait_for_state( |s| matches!(s, ProtocolContractState::Resharing(_)), @@ -641,7 +661,8 @@ impl MpcCluster { async fn vote_resharing( &self, current_participants: &Participants, - args: serde_json::Value, + prospective_epoch_id: EpochId, + proposal: ProposedGovernanceThresholdParameters, ) -> anyhow::Result<()> { let current_accounts: std::collections::HashSet<_> = current_participants .participants @@ -664,10 +685,10 @@ impl MpcCluster { } for i in participants_first.iter().chain(candidates_second.iter()) { - let client = self.operator_client_for(*i)?; let outcome = self .contract - .call_from(&client, method_names::VOTE_NEW_PARAMETERS, args.clone()) + .handle_for(self.operator_client_for(*i)?) + .vote_new_parameters(prospective_epoch_id, proposal.clone()) .await .with_context(|| format!("node {i} failed to send resharing vote"))?; if !outcome.is_success() { @@ -689,9 +710,9 @@ impl MpcCluster { &self, node_index: usize, ) -> anyhow::Result { - let client = self.operator_client_for(node_index)?; self.contract - .call_from(&client, method_names::VOTE_CANCEL_RESHARING, json!({})) + .handle_for(self.operator_client_for(node_index)?) + .vote_cancel_resharing() .await .with_context(|| format!("node {node_index} failed to send cancel resharing vote")) } @@ -758,40 +779,6 @@ impl MpcCluster { Ok(()) } - async fn call_from_all_nodes_concurrently( - &self, - method: &str, - args: serde_json::Value, - ) -> anyhow::Result<()> { - let clients: Vec<_> = self - .nodes - .iter() - .zip(self.node_keys.iter()) - .enumerate() - .filter(|(_, (node, _))| matches!(node, MpcNodeState::Running(_))) - .map(|(i, (node, key))| { - let client = self - .blockchain - .client_for(node.account_id().as_ref(), key)?; - Ok((i, node.account_id().clone(), client)) - }) - .collect::>>()?; - - let futures = clients.iter().map(|(i, account, client)| { - let args = args.clone(); - let method = method.to_string(); - async move { - self.contract - .call_from(client, &method, args) - .await - .with_context(|| format!("node {i} ({account}) failed to call {method}")) - } - }); - - futures::future::try_join_all(futures).await?; - Ok(()) - } - pub fn client_for(&self, account_id: &AccountId) -> anyhow::Result { let key = self .user_accounts @@ -871,17 +858,13 @@ impl MpcCluster { pub async fn register_backup_service( &self, node_index: usize, - backup_service_info: serde_json::Value, + backup_service_info: BackupServiceInfo, ) -> anyhow::Result { - let client = self.operator_client_for(node_index)?; self.contract - .call_from_deposit( - &client, - method_names::REGISTER_BACKUP_SERVICE, - json!({ "backup_service_info": backup_service_info }), - NODE_MANAGEMENT_DEPOSIT, - ) + .handle_for(self.operator_client_for(node_index)?) + .register_backup_service(backup_service_info) .await + .context("failed to register backup service") } /// View the foreign chains the contract accepts requests for. pub async fn view_foreign_chains_supported_by_contract( @@ -907,19 +890,11 @@ impl MpcCluster { node_index: usize, foreign_chain_support: &near_mpc_contract_interface::types::SupportedForeignChains, ) -> anyhow::Result { - let node = &self.nodes[node_index]; - let client = self - .blockchain - .client_for(node.account_id().as_ref(), &self.operator_keys[node_index])?; self.contract - .call_from( - &client, - method_names::REGISTER_FOREIGN_CHAIN_SUPPORT, - json!({ - "foreign_chain_support": serde_json::to_value(foreign_chain_support)?, - }), - ) + .handle_for(self.operator_client_for(node_index)?) + .register_foreign_chain_support(foreign_chain_support.clone()) .await + .context("failed to register foreign chain support") } pub async fn view_available_foreign_chains( @@ -997,13 +972,8 @@ impl MpcCluster { .operator_client_for(idx) .with_context(|| format!("whitelist_foreign_chains: node {idx}"))?; self.contract - .call_from_borsh_with_deposit( - &client, - method_names::VOTE_UPDATE_FOREIGN_CHAIN_PROVIDERS, - batch.clone(), - VOTE_FOREIGN_CHAIN_GAS, - near_kit::NearToken::from_yoctonear(0), - ) + .handle_for(client) + .vote_update_foreign_chain_providers(batch.clone()) .await .with_context(|| { format!("vote_update_foreign_chain_providers from node {idx} failed") @@ -1016,17 +986,13 @@ impl MpcCluster { pub async fn start_node_migration( &self, node_index: usize, - destination_node_info: serde_json::Value, + destination_node_info: DestinationNodeInfo, ) -> anyhow::Result { - let client = self.operator_client_for(node_index)?; self.contract - .call_from_deposit( - &client, - method_names::START_NODE_MIGRATION, - json!({ "destination_node_info": destination_node_info }), - NODE_MANAGEMENT_DEPOSIT, - ) + .handle_for(self.operator_client_for(node_index)?) + .start_node_migration(destination_node_info) .await + .context("failed to start node migration") } /// Update the registered URL of a specific node, called from that node's own operator account. @@ -1035,15 +1001,11 @@ impl MpcCluster { node_index: usize, url: String, ) -> anyhow::Result { - let client = self.operator_client_for(node_index)?; self.contract - .call_from_deposit( - &client, - method_names::UPDATE_PARTICIPANT_URL, - json!({ "url": url }), - NODE_MANAGEMENT_DEPOSIT, - ) + .handle_for(self.operator_client_for(node_index)?) + .update_participant_url(url) .await + .context("failed to update participant url") } /// Send a verify_foreign_transaction request from the default user account. @@ -1191,10 +1153,10 @@ impl MpcNodeState { } } - pub fn near_signer_public_key_str(&self) -> String { + pub fn near_signer_public_key(&self) -> Ed25519PublicKey { match self { - MpcNodeState::Running(n) => n.setup().near_signer_public_key_str(), - MpcNodeState::Stopped(s) => s.near_signer_public_key_str(), + MpcNodeState::Running(n) => n.setup().near_signer_public_key(), + MpcNodeState::Stopped(s) => s.near_signer_public_key(), } } } @@ -1335,13 +1297,13 @@ async fn add_initial_domains( domains: &[DomainConfig], ) -> anyhow::Result<()> { tracing::info!(count = domains.len(), "adding domains"); - let args = json!({ "domains": domains }); for &i in participant_indices { let account = format!("node{i}.{SANDBOX_ROOT_ACCOUNT}"); let client = blockchain.client_for(&account, &operator_keys[i])?; contract - .call_from(&client, method_names::VOTE_ADD_DOMAINS, args.clone()) + .handle_for(client) + .vote_add_domains(domains.to_vec()) .await .with_context(|| format!("node {i} failed to vote add domains"))?; } diff --git a/crates/e2e-tests/src/mpc_node.rs b/crates/e2e-tests/src/mpc_node.rs index 3dd3437463..f76053eb00 100644 --- a/crates/e2e-tests/src/mpc_node.rs +++ b/crates/e2e-tests/src/mpc_node.rs @@ -283,11 +283,8 @@ impl MpcNodeSetup { &self.near_signer_key } - /// The NEAR signer public key formatted as `"ed25519:"`. - pub fn near_signer_public_key_str(&self) -> String { - String::from(&Ed25519PublicKey::from( - &self.near_signer_key.verifying_key(), - )) + pub fn near_signer_public_key(&self) -> Ed25519PublicKey { + Ed25519PublicKey::from(&self.near_signer_key.verifying_key()) } /// Path to the mpc-node binary. diff --git a/crates/e2e-tests/tests/migration_endpoint.rs b/crates/e2e-tests/tests/migration_endpoint.rs index b8a586d66e..963065ccad 100644 --- a/crates/e2e-tests/tests/migration_endpoint.rs +++ b/crates/e2e-tests/tests/migration_endpoint.rs @@ -40,7 +40,6 @@ async fn migration_endpoint__should_track_migration_state() { }; let web_addr = node.web_address(); let account_id = node_state.account_id().to_string(); - let p2p_public_key = node_state.p2p_public_key_str(); // Given: the migration state carried over from prior iterations // (empty on the first iteration). @@ -53,9 +52,11 @@ async fn migration_endpoint__should_track_migration_state() { .expect("endpoint state mismatch"); // When: register a bogus backup service for this node. - let backup_service_info = serde_json::json!({ "public_key": p2p_public_key }); + let backup_info = BackupServiceInfo { + public_key: node_state.p2p_public_key(), + }; let outcome = cluster - .register_backup_service(i, backup_service_info) + .register_backup_service(i, backup_info.clone()) .await .expect("failed to register backup service"); assert!( @@ -63,9 +64,6 @@ async fn migration_endpoint__should_track_migration_state() { "register_backup_service failed: {:?}", outcome.failure_message() ); - let backup_info = BackupServiceInfo { - public_key: node_state.p2p_public_key(), - }; expected_migrations.insert(account_id.clone(), (Some(backup_info.clone()), None)); // Then: contract and endpoint both expose the backup registration. @@ -77,13 +75,13 @@ async fn migration_endpoint__should_track_migration_state() { .expect("endpoint state mismatch after backup registration"); // When: start node migration with a bogus destination. - let destination_node_info = serde_json::json!({ - "signer_account_pk": p2p_public_key, - "destination_node_info": { - "url": "http://bogus:1234", - "tls_public_key": p2p_public_key, + let destination_node_info = DestinationNodeInfo { + signer_account_pk: node_state.p2p_public_key(), + destination_node_info: ParticipantInfo { + url: "http://bogus:1234".to_string(), + tls_public_key: node_state.p2p_public_key(), }, - }); + }; let outcome = cluster .start_node_migration(i, destination_node_info) .await diff --git a/crates/e2e-tests/tests/migration_service.rs b/crates/e2e-tests/tests/migration_service.rs index 64092b8b95..35ae3be0c2 100644 --- a/crates/e2e-tests/tests/migration_service.rs +++ b/crates/e2e-tests/tests/migration_service.rs @@ -10,7 +10,8 @@ use anyhow::{Context, bail}; use backon::{ConstantBuilder, Retryable}; use e2e_tests::MpcNodeState; use near_mpc_contract_interface::types::{ - AccountId, BackupServiceInfo, DestinationNodeInfo, Ed25519PublicKey, ProtocolContractState, + AccountId, BackupServiceInfo, DestinationNodeInfo, Ed25519PublicKey, ParticipantInfo, + ProtocolContractState, }; use rand::SeedableRng; @@ -250,7 +251,9 @@ async fn register_backup_service_and_wait( let outcome = cluster .register_backup_service( source_idx, - serde_json::json!({ "public_key": backup_public_key }), + BackupServiceInfo { + public_key: backup_public_key.clone(), + }, ) .await .context("failed to register backup service")?; @@ -343,15 +346,15 @@ async fn start_migration_and_wait( let source_account_id = cluster.nodes[source_idx].account_id().to_string(); let target_p2p_key = cluster.nodes[target_idx].p2p_public_key(); let target_p2p_url = cluster.nodes[target_idx].p2p_url(); - let target_signer_pk = cluster.nodes[target_idx].near_signer_public_key_str(); + let target_signer_pk = cluster.nodes[target_idx].near_signer_public_key(); - let destination_node_info = serde_json::json!({ - "signer_account_pk": target_signer_pk, - "destination_node_info": { - "url": target_p2p_url, - "tls_public_key": target_p2p_key, + let destination_node_info = DestinationNodeInfo { + signer_account_pk: target_signer_pk, + destination_node_info: ParticipantInfo { + url: target_p2p_url, + tls_public_key: target_p2p_key.clone(), }, - }); + }; let outcome = cluster .start_node_migration(source_idx, destination_node_info) .await diff --git a/crates/e2e-tests/tests/web_endpoints.rs b/crates/e2e-tests/tests/web_endpoints.rs index aeb1ab0e78..09d9521c1e 100644 --- a/crates/e2e-tests/tests/web_endpoints.rs +++ b/crates/e2e-tests/tests/web_endpoints.rs @@ -212,7 +212,7 @@ async fn test_web_endpoints() { // transaction's outcome, which takes a few seconds. let row = vote_pk_row_regex( node.setup().account_id().as_ref(), - &node.setup().near_signer_public_key_str(), + &String::from(&node.setup().near_signer_public_key()), ); let expected_vote_pk_rows = running.domains.domains.len(); wait_for_body( diff --git a/crates/near-contract-transport/src/types.rs b/crates/near-contract-transport/src/types.rs index 6234c13d31..201eed81cc 100644 --- a/crates/near-contract-transport/src/types.rs +++ b/crates/near-contract-transport/src/types.rs @@ -12,6 +12,26 @@ pub struct FunctionCallArgs { pub deposit: NearToken, } +impl FunctionCallArgs { + pub fn new( + method_name: impl Into, + args: Vec, + gas: NearGas, + deposit: NearToken, + ) -> Self { + Self { + method_name: method_name.into(), + args, + gas, + deposit, + } + } + + pub fn no_deposit(method_name: impl Into, args: Vec, gas: NearGas) -> Self { + Self::new(method_name, args, gas, NearToken::from_yoctonear(0)) + } +} + #[derive(Debug, Clone)] pub struct ViewArgs { pub method_name: String, diff --git a/crates/near-mpc-contract-interface/src/call_args.rs b/crates/near-mpc-contract-interface/src/call_args.rs index b611ef01fb..4a60301fa6 100644 --- a/crates/near-mpc-contract-interface/src/call_args.rs +++ b/crates/near-mpc-contract-interface/src/call_args.rs @@ -1,10 +1,11 @@ //! Argument types for the NEAR MPC signer contract function calls. use crate::types::{ - Attestation, CKDRequest, CKDRequestArgs, CKDResponse, Ed25519PublicKey, KeyEventId, Keyset, - PublicKey, SignRequestArgs, SignatureRequest, SignatureResponse, - VerifyForeignTransactionRequest, VerifyForeignTransactionRequestArgs, - VerifyForeignTransactionResponse, + Attestation, BackupServiceInfo, CKDRequest, CKDRequestArgs, CKDResponse, DestinationNodeInfo, + DomainConfig, Ed25519PublicKey, EpochId, KeyEventId, Keyset, + ProposedGovernanceThresholdParameters, PublicKey, SignRequestArgs, SignatureRequest, + SignatureResponse, SupportedForeignChains, VerifyForeignTransactionRequest, + VerifyForeignTransactionRequestArgs, VerifyForeignTransactionResponse, }; use serde::{Deserialize, Serialize}; @@ -23,6 +24,42 @@ pub struct VerifyForeignTransactionArgs { pub request: VerifyForeignTransactionRequestArgs, } +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct VoteAddDomainsArgs { + pub domains: Vec, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct VoteNewParametersArgs { + pub prospective_epoch_id: EpochId, + pub proposal: ProposedGovernanceThresholdParameters, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct VoteCancelKeygenArgs { + pub next_domain_id: u64, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct UpdateParticipantUrlArgs { + pub url: String, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct RegisterBackupServiceArgs { + pub backup_service_info: BackupServiceInfo, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct StartNodeMigrationArgs { + pub destination_node_info: DestinationNodeInfo, +} + +#[derive(Serialize, Debug, derive_more::Constructor)] +pub struct RegisterForeignChainSupportArgs { + pub foreign_chain_support: SupportedForeignChains, +} + #[derive(Serialize, Debug, Deserialize, Clone, derive_more::Constructor)] pub struct SignatureRespondArgs { pub request: SignatureRequest, diff --git a/crates/near-mpc-contract-interface/src/client.rs b/crates/near-mpc-contract-interface/src/client.rs index 42d602c61a..7595e10d9d 100644 --- a/crates/near-mpc-contract-interface/src/client.rs +++ b/crates/near-mpc-contract-interface/src/client.rs @@ -7,21 +7,30 @@ use near_contract_transport::{CallContract, FunctionCallArgs, NearGas, NearToken}; use crate::call_args::{ - RequestAppPrivateKeyArgs, SignArgs, SubmitParticipantInfoArgs, VerifyForeignTransactionArgs, + RegisterBackupServiceArgs, RegisterForeignChainSupportArgs, RequestAppPrivateKeyArgs, SignArgs, + StartNodeMigrationArgs, SubmitParticipantInfoArgs, UpdateParticipantUrlArgs, + VerifyForeignTransactionArgs, VoteAddDomainsArgs, VoteCancelKeygenArgs, VoteNewParametersArgs, VoteUpdateArgs, }; use crate::deposits::{ - DepositOverflowError, SIGN_DEPOSIT_YOCTONEAR, STORAGE_BYTE_COST_YOCTONEAR, - SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, propose_update_required_deposit_yoctonear, + DepositOverflowError, MINIMUM_NODE_MANAGEMENT_DEPOSIT_YOCTONEAR, SIGN_DEPOSIT_YOCTONEAR, + STORAGE_BYTE_COST_YOCTONEAR, SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR, + propose_update_required_deposit_yoctonear, }; use crate::method_names::{ - PROPOSE_UPDATE, REQUEST_APP_PRIVATE_KEY, SIGN, SUBMIT_PARTICIPANT_INFO, - VERIFY_FOREIGN_TRANSACTION, VERIFY_TEE, VOTE_UPDATE, + PROPOSE_UPDATE, REGISTER_BACKUP_SERVICE, REGISTER_FOREIGN_CHAIN_SUPPORT, + REQUEST_APP_PRIVATE_KEY, SIGN, START_NODE_MIGRATION, SUBMIT_PARTICIPANT_INFO, + UPDATE_PARTICIPANT_URL, VERIFY_FOREIGN_TRANSACTION, VERIFY_TEE, VOTE_ADD_DOMAINS, + VOTE_CANCEL_KEYGEN, VOTE_CANCEL_RESHARING, VOTE_NEW_PARAMETERS, VOTE_UPDATE, + VOTE_UPDATE_FOREIGN_CHAIN_PROVIDERS, }; use crate::types::{ - AccountId, Attestation, CKDAppPublicKey, CKDRequestArgs, Ed25519PublicKey, PayloadBytesError, - ProposeUpdateArgs, SignRequestArgs, VerifyForeignTransactionRequestArgs, + AccountId, Attestation, BackupServiceInfo, CKDAppPublicKey, CKDRequestArgs, ChainEntry, + DestinationNodeInfo, DomainConfig, Ed25519PublicKey, EpochId, ForeignChain, PayloadBytesError, + ProposeUpdateArgs, ProposedGovernanceThresholdParameters, SignRequestArgs, + SupportedForeignChains, VerifyForeignTransactionRequestArgs, }; +use near_mpc_bounded_collections::NonEmptyBTreeMap; /// Default gas for handle-issued calls without a method-specific amount. // TODO(#166): 300 Tgas used to be the protocol maximum and higher than most methods @@ -33,6 +42,8 @@ pub const SIGN_GAS: NearGas = NearGas::from_tgas(15); // which costs significantly more than a plain CKD or sign request. pub const CKD_PV_GAS: NearGas = NearGas::from_tgas(100); +pub const VOTE_FOREIGN_CHAIN_GAS: NearGas = NearGas::from_tgas(30); + /// Typed interface to the MPC signer contract at a fixed account, generic over /// the transport backend `C`. #[derive(Clone)] @@ -56,18 +67,13 @@ impl MpcContractHandle { request: SignRequestArgs, ) -> Result> { let args = serde_json::to_vec(&SignArgs::new(request))?; - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: SIGN.to_string(), - args, - gas: SIGN_GAS, - deposit: NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::new( + SIGN, + args, + SIGN_GAS, + NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), + )) + .await } pub async fn request_app_private_key( @@ -79,18 +85,13 @@ impl MpcContractHandle { CKDAppPublicKey::AppPublicKeyPV(_) => CKD_PV_GAS, }; let args = serde_json::to_vec(&RequestAppPrivateKeyArgs::new(request))?; - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: REQUEST_APP_PRIVATE_KEY.to_string(), - args, - gas, - deposit: NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::new( + REQUEST_APP_PRIVATE_KEY, + args, + gas, + NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), + )) + .await } pub async fn verify_foreign_transaction( @@ -98,45 +99,32 @@ impl MpcContractHandle { request: VerifyForeignTransactionRequestArgs, ) -> Result> { let args = serde_json::to_vec(&VerifyForeignTransactionArgs::new(request))?; - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: VERIFY_FOREIGN_TRANSACTION.to_string(), - args, - gas: SIGN_GAS, - deposit: NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::new( + VERIFY_FOREIGN_TRANSACTION, + args, + SIGN_GAS, + NearToken::from_yoctonear(SIGN_DEPOSIT_YOCTONEAR), + )) + .await } pub async fn propose_update( &self, args: ProposeUpdateArgs, ) -> Result> { - let payload_bytes = args.payload_bytes().map_err(|err| match err { - PayloadBytesError::Serialize(err) => MpcContractHandleError::Serialize(err), - PayloadBytesError::Overflow => MpcContractHandleError::Deposit(DepositOverflowError), - })?; + let payload_bytes = args.payload_bytes()?; let deposit = NearToken::from_yoctonear(propose_update_required_deposit_yoctonear( payload_bytes, STORAGE_BYTE_COST_YOCTONEAR, )?); let args = borsh::to_vec(&args)?; - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: PROPOSE_UPDATE.to_string(), - args, - gas: MAX_GAS, - deposit, - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::new( + PROPOSE_UPDATE, + args, + MAX_GAS, + deposit, + )) + .await } pub async fn vote_update( @@ -144,16 +132,136 @@ impl MpcContractHandle { id: u64, ) -> Result> { let args = serde_json::to_vec(&VoteUpdateArgs::new(id))?; + self.call(FunctionCallArgs::no_deposit(VOTE_UPDATE, args, MAX_GAS)) + .await + } + + pub async fn vote_add_domains( + &self, + domains: Vec, + ) -> Result> { + let args = serde_json::to_vec(&VoteAddDomainsArgs::new(domains))?; + self.call(FunctionCallArgs::no_deposit( + VOTE_ADD_DOMAINS, + args, + MAX_GAS, + )) + .await + } + + pub async fn vote_new_parameters( + &self, + prospective_epoch_id: EpochId, + proposal: ProposedGovernanceThresholdParameters, + ) -> Result> { + let args = serde_json::to_vec(&VoteNewParametersArgs::new(prospective_epoch_id, proposal))?; + self.call(FunctionCallArgs::no_deposit( + VOTE_NEW_PARAMETERS, + args, + MAX_GAS, + )) + .await + } + + pub async fn vote_cancel_keygen( + &self, + next_domain_id: u64, + ) -> Result> { + let args = serde_json::to_vec(&VoteCancelKeygenArgs::new(next_domain_id))?; + self.call(FunctionCallArgs::no_deposit( + VOTE_CANCEL_KEYGEN, + args, + MAX_GAS, + )) + .await + } + + pub async fn vote_cancel_resharing( + &self, + ) -> Result> { + self.call(FunctionCallArgs::no_deposit( + VOTE_CANCEL_RESHARING, + b"{}".to_vec(), + MAX_GAS, + )) + .await + } + + pub async fn update_participant_url( + &self, + url: String, + ) -> Result> { + let args = serde_json::to_vec(&UpdateParticipantUrlArgs::new(url))?; + self.call(FunctionCallArgs::new( + UPDATE_PARTICIPANT_URL, + args, + MAX_GAS, + NearToken::from_yoctonear(MINIMUM_NODE_MANAGEMENT_DEPOSIT_YOCTONEAR), + )) + .await + } + + pub async fn register_backup_service( + &self, + backup_service_info: BackupServiceInfo, + ) -> Result> { + let args = serde_json::to_vec(&RegisterBackupServiceArgs::new(backup_service_info))?; + self.call(FunctionCallArgs::new( + REGISTER_BACKUP_SERVICE, + args, + MAX_GAS, + NearToken::from_yoctonear(MINIMUM_NODE_MANAGEMENT_DEPOSIT_YOCTONEAR), + )) + .await + } + + pub async fn start_node_migration( + &self, + destination_node_info: DestinationNodeInfo, + ) -> Result> { + let args = serde_json::to_vec(&StartNodeMigrationArgs::new(destination_node_info))?; + self.call(FunctionCallArgs::new( + START_NODE_MIGRATION, + args, + MAX_GAS, + NearToken::from_yoctonear(MINIMUM_NODE_MANAGEMENT_DEPOSIT_YOCTONEAR), + )) + .await + } + + pub async fn register_foreign_chain_support( + &self, + foreign_chain_support: SupportedForeignChains, + ) -> Result> { + let args = + serde_json::to_vec(&RegisterForeignChainSupportArgs::new(foreign_chain_support))?; + self.call(FunctionCallArgs::no_deposit( + REGISTER_FOREIGN_CHAIN_SUPPORT, + args, + MAX_GAS, + )) + .await + } + + pub async fn vote_update_foreign_chain_providers( + &self, + batch: NonEmptyBTreeMap, + ) -> Result> { + let args = borsh::to_vec(&batch)?; + self.call(FunctionCallArgs::no_deposit( + VOTE_UPDATE_FOREIGN_CHAIN_PROVIDERS, + args, + VOTE_FOREIGN_CHAIN_GAS, + )) + .await + } + + async fn call( + &self, + call_args: FunctionCallArgs, + ) -> Result> { self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: VOTE_UPDATE.to_string(), - args, - gas: MAX_GAS, - deposit: NearToken::from_yoctonear(0), - }, - ) + .call_contract(&self.contract_id, call_args) .await .map_err(MpcContractHandleError::Call) } @@ -167,33 +275,22 @@ impl MpcContractHandle { proposed_participant_attestation, tls_public_key, ))?; - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: SUBMIT_PARTICIPANT_INFO.to_string(), - args, - gas: MAX_GAS, - deposit: NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR), - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::new( + SUBMIT_PARTICIPANT_INFO, + args, + MAX_GAS, + NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR), + )) + .await } pub async fn verify_tee(&self) -> Result> { - self.caller - .call_contract( - &self.contract_id, - FunctionCallArgs { - method_name: VERIFY_TEE.to_string(), - args: b"{}".to_vec(), - gas: MAX_GAS, - deposit: NearToken::from_yoctonear(0), - }, - ) - .await - .map_err(MpcContractHandleError::Call) + self.call(FunctionCallArgs::no_deposit( + VERIFY_TEE, + b"{}".to_vec(), + MAX_GAS, + )) + .await } } @@ -209,18 +306,33 @@ pub enum MpcContractHandleError { Call(E), } +impl From for MpcContractHandleError { + fn from(value: PayloadBytesError) -> Self { + match value { + PayloadBytesError::Serialize(err) => MpcContractHandleError::Serialize(err), + PayloadBytesError::Overflow => MpcContractHandleError::Deposit(DepositOverflowError), + } + } +} + #[cfg(test)] #[expect(non_snake_case)] mod tests { use super::MpcContractHandle; use crate::types::{ - AccountId, Attestation, BitcoinExtractor, BitcoinRpcRequest, BitcoinTxId, - BlockConfirmations, CKDAppPublicKey, CKDAppPublicKeyPV, CKDRequestArgs, DomainId, - Ed25519PublicKey, ForeignChainRpcRequest, ForeignTxPayloadVersion, MockAttestation, - Payload, ProposeUpdateArgs, SignRequestArgs, VerifyForeignTransactionRequestArgs, + AccountId, Attestation, AuthScheme, BackupServiceInfo, BitcoinExtractor, BitcoinRpcRequest, + BitcoinTxId, BlockConfirmations, CKDAppPublicKey, CKDAppPublicKeyPV, CKDRequestArgs, + ChainEntry, ChainRouting, DestinationNodeInfo, DomainConfig, DomainId, DomainPurpose, + Ed25519PublicKey, EpochId, ForeignChain, ForeignChainRpcRequest, ForeignTxPayloadVersion, + GovernanceThreshold, GovernanceThresholdParameters, MockAttestation, ParticipantId, + ParticipantInfo, Participants, Payload, ProposeUpdateArgs, + ProposedGovernanceThresholdParameters, Protocol, ProviderConfig, ProviderId, + ReconstructionThreshold, SignRequestArgs, VerifyForeignTransactionRequestArgs, }; use near_contract_transport::{CallContract, FunctionCallArgs}; + use near_mpc_bounded_collections::NonEmptyBTreeMap; use near_mpc_crypto_types::{Bls12381G1PublicKey, Bls12381G2PublicKey}; + use std::collections::{BTreeMap, BTreeSet}; use std::sync::Mutex; /// A [`CallContract`] that records the calls it is handed, so a test can @@ -324,6 +436,81 @@ mod tests { .await .unwrap(); handle.vote_update(7).await.unwrap(); + handle + .vote_add_domains(vec![DomainConfig { + id: DomainId(0), + protocol: Protocol::CaitSith, + reconstruction_threshold: ReconstructionThreshold::new(2), + purpose: DomainPurpose::Sign, + }]) + .await + .unwrap(); + handle + .vote_new_parameters( + EpochId::new(7), + ProposedGovernanceThresholdParameters { + parameters: GovernanceThresholdParameters { + threshold: GovernanceThreshold(1), + participants: Participants { + next_id: ParticipantId(1), + participants: vec![( + "alice.near".parse().unwrap(), + ParticipantId(0), + ParticipantInfo { + url: "http://localhost:7".to_string(), + tls_public_key: Ed25519PublicKey::from([7u8; 32]), + }, + )], + }, + }, + per_domain_thresholds: BTreeMap::new(), + }, + ) + .await + .unwrap(); + handle.vote_cancel_keygen(7).await.unwrap(); + handle.vote_cancel_resharing().await.unwrap(); + handle + .update_participant_url("http://localhost:7".to_string()) + .await + .unwrap(); + handle + .register_backup_service(BackupServiceInfo { + public_key: Ed25519PublicKey::from([7u8; 32]), + }) + .await + .unwrap(); + handle + .start_node_migration(DestinationNodeInfo { + signer_account_pk: Ed25519PublicKey::from([7u8; 32]), + destination_node_info: ParticipantInfo { + url: "http://localhost:7".to_string(), + tls_public_key: Ed25519PublicKey::from([7u8; 32]), + }, + }) + .await + .unwrap(); + handle + .register_foreign_chain_support(BTreeSet::from([ForeignChain::Bitcoin]).into()) + .await + .unwrap(); + handle + .vote_update_foreign_chain_providers(NonEmptyBTreeMap::new( + ForeignChain::Bitcoin, + ChainEntry { + providers: NonEmptyBTreeMap::new( + ProviderId("alchemy".to_string()), + ProviderConfig { + base_url: "http://localhost:7".to_string(), + auth_scheme: AuthScheme::None, + chain_routing: ChainRouting::Embedded, + }, + ), + quorum: 1, + }, + )) + .await + .unwrap(); handle .submit_participant_info( Attestation::Mock(MockAttestation::Valid), @@ -335,7 +522,7 @@ mod tests { // Then let calls = caller.calls.lock().unwrap(); - assert_eq!(calls.len(), 8); + assert_eq!(calls.len(), 17); let catalog = calls .iter() .map(|(contract_id, call)| render(contract_id, call)) diff --git a/crates/near-mpc-contract-interface/src/deposits.rs b/crates/near-mpc-contract-interface/src/deposits.rs index 45b6b4c8f5..eff93e2563 100644 --- a/crates/near-mpc-contract-interface/src/deposits.rs +++ b/crates/near-mpc-contract-interface/src/deposits.rs @@ -11,6 +11,8 @@ pub const STORAGE_BYTE_COST_YOCTONEAR: u128 = 10_000_000_000_000_000_000; pub const PROPOSE_UPDATE_ENTRY_OVERHEAD_BYTES: u128 = 32_768; +pub const MINIMUM_NODE_MANAGEMENT_DEPOSIT_YOCTONEAR: u128 = 1; + #[derive(Debug, PartialEq, Eq, thiserror::Error)] #[error("the required deposit exceeds u128::MAX yoctoNEAR")] pub struct DepositOverflowError; diff --git a/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_wire_format_catalog.snap b/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_wire_format_catalog.snap index 766e07136c..ed139a8be2 100644 --- a/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_wire_format_catalog.snap +++ b/crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_wire_format_catalog.snap @@ -38,6 +38,60 @@ gas: 300.0 Tgas deposit: 0 NEAR args: {"id":7} +contract: mpc.near +method: vote_add_domains +gas: 300.0 Tgas +deposit: 0 NEAR +args: {"domains":[{"id":0,"protocol":"CaitSith","reconstruction_threshold":2,"purpose":"Sign"}]} + +contract: mpc.near +method: vote_new_parameters +gas: 300.0 Tgas +deposit: 0 NEAR +args: {"prospective_epoch_id":7,"proposal":{"parameters":{"participants":{"next_id":1,"participants":[["alice.near",0,{"url":"http://localhost:7","tls_public_key":"ed25519:US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx"}]]},"threshold":1},"per_domain_thresholds":{}}} + +contract: mpc.near +method: vote_cancel_keygen +gas: 300.0 Tgas +deposit: 0 NEAR +args: {"next_domain_id":7} + +contract: mpc.near +method: vote_cancel_resharing +gas: 300.0 Tgas +deposit: 0 NEAR +args: {} + +contract: mpc.near +method: update_participant_url +gas: 300.0 Tgas +deposit: 1 yoctoNEAR +args: {"url":"http://localhost:7"} + +contract: mpc.near +method: register_backup_service +gas: 300.0 Tgas +deposit: 1 yoctoNEAR +args: {"backup_service_info":{"public_key":"ed25519:US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx"}} + +contract: mpc.near +method: start_node_migration +gas: 300.0 Tgas +deposit: 1 yoctoNEAR +args: {"destination_node_info":{"signer_account_pk":"ed25519:US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx","destination_node_info":{"url":"http://localhost:7","tls_public_key":"ed25519:US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx"}}} + +contract: mpc.near +method: register_foreign_chain_support +gas: 300.0 Tgas +deposit: 0 NEAR +args: {"foreign_chain_support":["Bitcoin"]} + +contract: mpc.near +method: vote_update_foreign_chain_providers +gas: 30.0 Tgas +deposit: 0 NEAR +args: 0x01000000010100000007000000616c6368656d7912000000687474703a2f2f6c6f63616c686f73743a3703000100000000000000 + contract: mpc.near method: submit_participant_info gas: 300.0 Tgas