diff --git a/Cargo.lock b/Cargo.lock index e8e3592c148..e0027be2e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3217,6 +3217,7 @@ dependencies = [ "curve25519-dalek", "derive_more", "ed25519-dalek", + "hex", "hex-literal", "near-account-id", "near-config-utils", @@ -5991,6 +5992,7 @@ dependencies = [ "near-crypto", "near-primitives", "near-test-contracts", + "node-runtime", "once_cell", ] diff --git a/core/crypto/Cargo.toml b/core/crypto/Cargo.toml index 2a0a0ac9c1c..c6d4947f631 100644 --- a/core/crypto/Cargo.toml +++ b/core/crypto/Cargo.toml @@ -18,6 +18,7 @@ c2-chacha.workspace = true curve25519-dalek.workspace = true derive_more.workspace = true ed25519-dalek.workspace = true +hex.workspace = true near-account-id = { path = "../account-id" } once_cell.workspace = true primitive-types.workspace = true diff --git a/core/crypto/src/test_utils.rs b/core/crypto/src/test_utils.rs index 08e8435f5fa..7e15e80ca80 100644 --- a/core/crypto/src/test_utils.rs +++ b/core/crypto/src/test_utils.rs @@ -1,3 +1,4 @@ +use borsh::BorshDeserialize; use secp256k1::rand::SeedableRng; use crate::signature::{ED25519PublicKey, ED25519SecretKey, KeyType, PublicKey, SecretKey}; @@ -33,6 +34,19 @@ impl PublicKey { _ => unimplemented!(), } } + + pub fn from_implicit_account(account_id: &AccountId) -> Self { + assert!(account_id.is_implicit()); + let mut public_key_data = Vec::with_capacity(33); + public_key_data.push(KeyType::ED25519 as u8); + public_key_data.extend( + hex::decode(account_id.as_ref().as_bytes()) + .expect("account id was a valid hex of length 64 resulting in 32 bytes"), + ); + assert_eq!(public_key_data.len(), 33); + PublicKey::try_from_slice(&public_key_data) + .expect("we should be able to deserialize ED25519 public key") + } } impl SecretKey { diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 469f6d9efe3..211e67bf4e5 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -468,6 +468,9 @@ pub enum ActionErrorKind { /// Error occurs when a `CreateAccount` action is called on hex-characters /// account of length 64. See implicit account creation NEP: /// . + /// + /// TODO(#8598): This error is named very poorly. A better name would be + /// `OnlyNamedAccountCreationAllowed`. OnlyImplicitAccountCreationAllowed { account_id: AccountId }, /// Delete account whose state is large is temporarily banned. DeleteAccountWithLargeState { account_id: AccountId }, diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 6e816957c98..ef9ff49fc4b 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use near_crypto::{EmptySigner, InMemorySigner, KeyType, PublicKey, Signature, Signer}; +use near_crypto::{EmptySigner, InMemorySigner, KeyType, PublicKey, SecretKey, Signature, Signer}; use near_primitives_core::types::ProtocolVersion; use crate::account::{AccessKey, AccessKeyPermission, Account}; @@ -491,9 +491,26 @@ pub fn create_test_signer(account_name: &str) -> InMemoryValidatorSigner { /// Helper function that creates a new signer for a given account, that uses the account name as seed. /// +/// This also works for predefined implicit accounts, where the signer will use the implicit key. +/// /// Should be used only in tests. pub fn create_user_test_signer(account_name: &str) -> InMemorySigner { - InMemorySigner::from_seed(account_name.parse().unwrap(), KeyType::ED25519, account_name) + let account_id = account_name.parse().unwrap(); + if account_id == implicit_test_account() { + InMemorySigner::from_secret_key(account_id, implicit_test_account_secret()) + } else { + InMemorySigner::from_seed(account_id, KeyType::ED25519, account_name) + } +} + +/// A fixed implicit account for which tests can know the private key. +pub fn implicit_test_account() -> AccountId { + "061b1dd17603213b00e1a1e53ba060ad427cef4887bd34a5e0ef09010af23b0a".parse().unwrap() +} + +/// Private key for the fixed implicit test account. +pub fn implicit_test_account_secret() -> SecretKey { + "ed25519:5roj6k68kvZu3UEJFyXSfjdKGrodgZUfFLZFpzYXWtESNsLWhYrq3JGi4YpqeVKuw1m9R2TEHjfgWT1fjUqB1DNy".parse().unwrap() } impl FinalExecutionOutcomeView { diff --git a/integration-tests/src/tests/client/features/delegate_action.rs b/integration-tests/src/tests/client/features/delegate_action.rs index 20d8861519c..e8245d7e0c0 100644 --- a/integration-tests/src/tests/client/features/delegate_action.rs +++ b/integration-tests/src/tests/client/features/delegate_action.rs @@ -12,18 +12,20 @@ use near_client::test_utils::TestEnv; use near_crypto::{KeyType, PublicKey}; use near_primitives::account::AccessKey; use near_primitives::config::ActionCosts; -use near_primitives::errors::{ActionsValidationError, InvalidTxError, TxExecutionError}; -use near_primitives::test_utils::create_user_test_signer; +use near_primitives::errors::{ + ActionError, ActionErrorKind, ActionsValidationError, InvalidTxError, TxExecutionError, +}; +use near_primitives::test_utils::{create_user_test_signer, implicit_test_account}; use near_primitives::transaction::{ - Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, - FunctionCallAction, StakeAction, TransferAction, + Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, + DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives::views::{ AccessKeyPermissionView, FinalExecutionOutcomeView, FinalExecutionStatus, }; -use near_test_contracts::smallest_rs_contract; +use near_test_contracts::{ft_contract, smallest_rs_contract}; use nearcore::config::GenesisExt; use nearcore::NEAR_BASE; use testlib::runtime_utils::{ @@ -117,15 +119,17 @@ fn check_meta_tx_execution( let sender_before = node_user.view_balance(&sender).unwrap(); let relayer_before = node_user.view_balance(&relayer).unwrap(); - let receiver_before = node_user.view_balance(&receiver).unwrap(); + let receiver_before = node_user.view_balance(&receiver).unwrap_or(0); let relayer_nonce_before = node_user .get_access_key(&relayer, &PublicKey::from_seed(KeyType::ED25519, &relayer)) .unwrap() .nonce; - let user_nonce_before = node_user - .get_access_key(&sender, &PublicKey::from_seed(KeyType::ED25519, &sender)) - .unwrap() - .nonce; + let user_pubk = if sender.is_implicit() { + PublicKey::from_implicit_account(&sender) + } else { + PublicKey::from_seed(KeyType::ED25519, &sender) + }; + let user_nonce_before = node_user.get_access_key(&sender, &user_pubk).unwrap().nonce; let tx_result = node_user .meta_tx(sender.clone(), receiver.clone(), relayer.clone(), actions.clone()) @@ -171,7 +175,7 @@ fn check_meta_tx_no_fn_call( receiver: AccountId, ) -> FinalExecutionOutcomeView { let fee_helper = fee_helper(node); - let gas_cost = normal_tx_cost + fee_helper.meta_tx_overhead_cost(&actions); + let gas_cost = normal_tx_cost + fee_helper.meta_tx_overhead_cost(&actions, &receiver); let (tx_result, sender_diff, relayer_diff, receiver_diff) = check_meta_tx_execution(node, actions, sender, relayer, receiver); @@ -203,7 +207,7 @@ fn check_meta_tx_fn_call( ) -> FinalExecutionOutcomeView { let fee_helper = fee_helper(node); let num_fn_calls = actions.len(); - let meta_tx_overhead_cost = fee_helper.meta_tx_overhead_cost(&actions); + let meta_tx_overhead_cost = fee_helper.meta_tx_overhead_cost(&actions, &receiver); let (tx_result, sender_diff, relayer_diff, receiver_diff) = check_meta_tx_execution(node, actions, sender, relayer, receiver); @@ -401,8 +405,8 @@ fn meta_tx_delete_account() { vec![Action::DeleteAccount(DeleteAccountAction { beneficiary_id: relayer.clone() })]; // special case balance check for deleting account - let gas_cost = - fee_helper.prepaid_delete_account_cost() + fee_helper.meta_tx_overhead_cost(&actions); + let gas_cost = fee_helper.prepaid_delete_account_cost() + + fee_helper.meta_tx_overhead_cost(&actions, &receiver); let (_tx_result, sender_diff, relayer_diff, receiver_diff) = check_meta_tx_execution(&node, actions, sender, relayer, receiver.clone()); @@ -572,3 +576,151 @@ fn assert_ft_balance( let balance = std::str::from_utf8(&response.result).expect("invalid UTF8"); assert_eq!(format!("\"{expected_balance}\""), balance); } + +/// Test account creation scenarios with meta transactions. +/// +/// Named accounts aren't the primary use case for meta transactions but still +/// worth a test case. +#[test] +fn meta_tx_create_named_account() { + let relayer = bob_account(); + let sender = alice_account(); + let new_account = eve_dot_alice_account(); + let node = RuntimeNode::new(&relayer); + + let fee_helper = fee_helper(&node); + let amount = NEAR_BASE; + + let public_key = PublicKey::from_seed(KeyType::ED25519, &new_account); + + // That's the minimum to create a (useful) account. + let actions = vec![ + Action::CreateAccount(CreateAccountAction {}), + Action::Transfer(TransferAction { deposit: amount }), + Action::AddKey(AddKeyAction { public_key, access_key: AccessKey::full_access() }), + ]; + + // Check the account doesn't exist, yet. We want to create it. + node.view_account(&new_account).expect_err("account already exists"); + + let tx_cost = fee_helper.create_account_transfer_full_key_cost(); + check_meta_tx_no_fn_call(&node, actions, tx_cost, amount, sender, relayer, new_account.clone()); + + // Check the account exists after we created it. + node.view_account(&new_account).expect("failed looking up account"); +} + +/// Try creating an implicit account with `CreateAction` which is not allowed in +/// or outside meta transactions and must fail with `OnlyImplicitAccountCreationAllowed`. +#[test] +fn meta_tx_create_implicit_account_fails() { + let relayer = bob_account(); + let sender = alice_account(); + let new_account: AccountId = implicit_test_account(); + let node = RuntimeNode::new(&relayer); + + let actions = vec![Action::CreateAccount(CreateAccountAction {})]; + let tx_result = node + .user() + .meta_tx(sender.clone(), new_account.clone(), relayer.clone(), actions.clone()) + .unwrap(); + + let account_creation_result = &tx_result.receipts_outcome[1].outcome.status; + assert!(matches!( + account_creation_result, + near_primitives::views::ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::OnlyImplicitAccountCreationAllowed { .. }, .. } + )), + )); +} + +/// Try creating an implicit account with a meta tx transfer and use the account +/// in the same meta transaction. +/// +/// This is expected to fail with `AccountDoesNotExist`, known limitation of NEP-366. +/// It only works with accounts that already exists because it needs to do a +/// nonce check against the access key, which can only exist if the account exists. +#[test] +fn meta_tx_create_and_use_implicit_account() { + let relayer = bob_account(); + let sender = alice_account(); + let new_account: AccountId = implicit_test_account(); + let node = RuntimeNode::new(&relayer); + + // Check the account doesn't exist, yet. We will attempt creating it. + node.view_account(&new_account).expect_err("account already exists"); + + let initial_amount = nearcore::NEAR_BASE; + let actions = vec![ + Action::Transfer(TransferAction { deposit: initial_amount }), + Action::DeployContract(DeployContractAction { code: ft_contract().to_vec() }), + ]; + + // Execute and expect `AccountDoesNotExist`, as we try to call a meta + // transaction on a user that doesn't exist yet. + let tx_result = + node.user().meta_tx(sender.clone(), new_account.clone(), relayer.clone(), actions).unwrap(); + let status = &tx_result.receipts_outcome[1].outcome.status; + assert!(matches!( + status, + near_primitives::views::ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::AccountDoesNotExist { account_id }, .. } + )) if *account_id == new_account, + )); +} + +/// Creating an implicit account with a meta tx transfer and use the account in +/// a second meta transaction. +/// +/// Creation through a meta tx should work as normal, it's just that the relayer +/// pays for the storage and the user could delete the account and cash in, +/// hence this workflow is not ideal from all circumstances. +#[test] +fn meta_tx_create_implicit_account() { + let relayer = bob_account(); + let sender = alice_account(); + let new_account: AccountId = implicit_test_account(); + let node = RuntimeNode::new(&relayer); + + // Check account doesn't exist, yet + node.view_account(&new_account).expect_err("account already exists"); + + let fee_helper = fee_helper(&node); + let initial_amount = nearcore::NEAR_BASE; + let actions = vec![Action::Transfer(TransferAction { deposit: initial_amount })]; + let tx_cost = fee_helper.create_account_transfer_full_key_cost(); + check_meta_tx_no_fn_call( + &node, + actions, + tx_cost, + initial_amount, + sender.clone(), + relayer.clone(), + new_account.clone(), + ); + + // Check account exists with expected balance + node.view_account(&new_account).expect("failed looking up account"); + let balance = node.view_balance(&new_account).expect("failed looking up balance"); + assert_eq!(balance, initial_amount); + + // Now test we can use this account in a meta transaction that sends back half the tokens to alice. + let transfer_amount = initial_amount / 2; + let actions = vec![Action::Transfer(TransferAction { deposit: transfer_amount })]; + let tx_cost = fee_helper.transfer_cost(); + check_meta_tx_no_fn_call( + &node, + actions, + tx_cost, + transfer_amount, + new_account.clone(), + relayer, + sender, + ) + .assert_success(); + + // balance of the new account should NOT change, the relayer pays for it! + // (note: relayer balance checks etc are done in the shared checker function) + let balance = node.view_balance(&new_account).expect("failed looking up balance"); + assert_eq!(balance, initial_amount); +} diff --git a/test-utils/testlib/Cargo.toml b/test-utils/testlib/Cargo.toml index 1f6f54a089e..a991f382401 100644 --- a/test-utils/testlib/Cargo.toml +++ b/test-utils/testlib/Cargo.toml @@ -13,6 +13,7 @@ near-chain = { path = "../../chain/chain" } near-crypto = { path = "../../core/crypto" } near-primitives = { path = "../../core/primitives" } near-test-contracts = { path = "../../runtime/near-test-contracts" } +node-runtime = { path = "../../runtime/runtime" } [features] default = [] diff --git a/test-utils/testlib/src/fees_utils.rs b/test-utils/testlib/src/fees_utils.rs index 935273a0049..0e0b3ed76a0 100644 --- a/test-utils/testlib/src/fees_utils.rs +++ b/test-utils/testlib/src/fees_utils.rs @@ -1,15 +1,11 @@ //! Helper functions to compute the costs of certain actions assuming they succeed and the only //! actions in the transaction batch. -#[cfg(feature = "protocol_feature_nep366_delegate_action")] -use near_primitives::account::AccessKeyPermission; -#[cfg(feature = "protocol_feature_nep366_delegate_action")] -use near_primitives::account::{AccessKey, FunctionCallPermission}; use near_primitives::config::ActionCosts; use near_primitives::runtime::fees::RuntimeFeesConfig; #[cfg(feature = "protocol_feature_nep366_delegate_action")] -use near_primitives::transaction::{ - Action, AddKeyAction, DeployContractAction, FunctionCallAction, -}; +use near_primitives::transaction::Action; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_primitives::types::AccountId; use near_primitives::types::{Balance, Gas}; pub struct FeeHelper { @@ -176,49 +172,24 @@ impl FeeHelper { /// - The base cost of the delegate action (send and exec). /// - The additional send cost for all inner actions. #[cfg(feature = "protocol_feature_nep366_delegate_action")] - pub fn meta_tx_overhead_cost(&self, actions: &[Action]) -> Balance { + pub fn meta_tx_overhead_cost(&self, actions: &[Action], receiver: &AccountId) -> Balance { // for tests, we assume sender != receiver + + use near_primitives::version::PROTOCOL_VERSION; let sir = false; let base = self.cfg.fee(ActionCosts::delegate); let receipt = self.cfg.fee(ActionCosts::new_action_receipt); - let mut total_gas = base.exec_fee() + base.send_fee(false) + receipt.send_fee(sir); - for action in actions { - let send_gas = match action { - Action::CreateAccount(_) => self.cfg.fee(ActionCosts::create_account).send_fee(sir), - Action::DeployContract(DeployContractAction { code }) => { - self.cfg.fee(ActionCosts::deploy_contract_base).send_fee(sir) - + self.cfg.fee(ActionCosts::deploy_contract_byte).send_fee(sir) - * code.len() as u64 - } - Action::FunctionCall(FunctionCallAction { method_name, args, .. }) => { - let num_bytes = method_name.len() + args.len(); - self.cfg.fee(ActionCosts::function_call_base).send_fee(sir) - + self.cfg.fee(ActionCosts::function_call_byte).send_fee(sir) - * num_bytes as u64 - } - Action::Transfer(_) => self.cfg.fee(ActionCosts::transfer).send_fee(sir), - Action::Stake(_) => self.cfg.fee(ActionCosts::stake).send_fee(sir), - Action::AddKey(AddKeyAction { - access_key: AccessKey { permission, .. }, .. - }) => match permission { - AccessKeyPermission::FunctionCall(FunctionCallPermission { - method_names, - .. - }) => { - self.cfg.fee(ActionCosts::add_function_call_key_base).send_fee(sir) - + self.cfg.fee(ActionCosts::add_function_call_key_byte).send_fee(sir) - * method_names.len() as u64 - } - AccessKeyPermission::FullAccess => { - self.cfg.fee(ActionCosts::add_full_access_key).send_fee(sir) - } - }, - Action::DeleteKey(_) => self.cfg.fee(ActionCosts::delete_key).send_fee(sir), - Action::DeleteAccount(_) => self.cfg.fee(ActionCosts::delete_account).send_fee(sir), - Action::Delegate(_) => self.cfg.fee(ActionCosts::delegate).send_fee(sir), - }; - total_gas += send_gas; - } + let total_gas = base.exec_fee() + + base.send_fee(sir) + + receipt.send_fee(sir) + + node_runtime::config::total_send_fees( + &self.cfg, + sir, + actions, + receiver, + PROTOCOL_VERSION, + ) + .unwrap(); self.gas_to_balance(total_gas) } }