From 3a77c2abb6ff4151725df021d7399045544102a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 21 Nov 2023 15:51:06 +0100 Subject: [PATCH] Wallet Contract placeholder tests --- core/crypto/src/signature.rs | 7 ++ core/crypto/src/test_utils.rs | 5 +- core/primitives/src/test_utils.rs | 5 + core/primitives/src/utils.rs | 1 + .../access_key_nonce_for_implicit_accounts.rs | 110 +++++++++++++++++- .../tests/client/features/delegate_action.rs | 79 ++++++++++--- .../src/tests/standard_cases/mod.rs | 2 +- integration-tests/src/user/mod.rs | 3 + integration-tests/src/user/rpc_user.rs | 10 ++ integration-tests/src/user/runtime_user.rs | 8 ++ 10 files changed, 207 insertions(+), 23 deletions(-) diff --git a/core/crypto/src/signature.rs b/core/crypto/src/signature.rs index 966559c1fb5..c9010dd6b5b 100644 --- a/core/crypto/src/signature.rs +++ b/core/crypto/src/signature.rs @@ -156,6 +156,13 @@ impl PublicKey { Self::SECP256K1(_) => panic!(), } } + + pub fn unwrap_as_secp256k1(&self) -> &Secp256K1PublicKey { + match self { + Self::SECP256K1(key) => key, + Self::ED25519(_) => panic!(), + } + } } // This `Hash` implementation is safe since it retains the property diff --git a/core/crypto/src/test_utils.rs b/core/crypto/src/test_utils.rs index 08e8435f5fa..3d325085384 100644 --- a/core/crypto/src/test_utils.rs +++ b/core/crypto/src/test_utils.rs @@ -30,7 +30,10 @@ impl PublicKey { let keypair = ed25519_key_pair_from_seed(seed); PublicKey::ED25519(ED25519PublicKey(keypair.public.to_bytes())) } - _ => unimplemented!(), + KeyType::SECP256K1 => { + let secret_key = SecretKey::SECP256K1(secp256k1_secret_key_from_seed(seed)); + PublicKey::SECP256K1(secret_key.public_key().unwrap_as_secp256k1().clone()) + } } } } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 0aaf94ec599..959eb347e48 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -561,6 +561,11 @@ pub fn near_implicit_test_account_secret() -> SecretKey { "ed25519:5roj6k68kvZu3UEJFyXSfjdKGrodgZUfFLZFpzYXWtESNsLWhYrq3JGi4YpqeVKuw1m9R2TEHjfgWT1fjUqB1DNy".parse().unwrap() } +/// A fixed ETH-implicit account. +pub fn eth_implicit_test_account() -> AccountId { + "0x96791e923f8cf697ad9c3290f2c9059f0231b24c".parse().unwrap() +} + impl FinalExecutionOutcomeView { #[track_caller] /// Check transaction and all transitive receipts for success status. diff --git a/core/primitives/src/utils.rs b/core/primitives/src/utils.rs index 5ebd1424047..394b70b138b 100644 --- a/core/primitives/src/utils.rs +++ b/core/primitives/src/utils.rs @@ -489,6 +489,7 @@ pub fn account_is_implicit(account_id: &AccountId, protocol_version: ProtocolVer } /// Derives `AccountId` from `PublicKey`. +/// If the key type is ED25519, returns hex-encoded copy of the key. /// If the key type is SECP256K1, returns '0x' + keccak256(public_key)[12:32].hex(). pub fn derive_account_id_from_public_key(public_key: &PublicKey) -> AccountId { match public_key.key_type() { diff --git a/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs b/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs index 7d8acccd62e..eb3715778e7 100644 --- a/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs +++ b/integration-tests/src/tests/client/features/access_key_nonce_for_implicit_accounts.rs @@ -12,14 +12,15 @@ use near_network::shards_manager::ShardsManagerRequestFromNetwork; use near_network::types::{NetworkRequests, PeerManagerMessageRequest}; use near_o11y::testonly::init_test_logger; use near_primitives::account::AccessKey; -use near_primitives::errors::InvalidTxError; +use near_primitives::checked_feature; +use near_primitives::errors::{InvalidAccessKeyError, InvalidTxError}; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::shard_layout::ShardLayout; use near_primitives::sharding::ChunkHash; -use near_primitives::transaction::SignedTransaction; +use near_primitives::transaction::{Action, AddKeyAction, DeployContractAction, SignedTransaction}; use near_primitives::types::{AccountId, BlockHeight}; -use near_primitives::utils::derive_account_id_from_public_key; -use near_primitives::version::{ProtocolFeature, ProtocolVersion}; +use near_primitives::utils::{derive_account_id_from_public_key, wallet_contract_placeholder}; +use near_primitives::version::{ProtocolFeature, ProtocolVersion, PROTOCOL_VERSION}; use near_primitives::views::FinalExecutionStatus; use nearcore::config::GenesisExt; use nearcore::test_utils::TestEnvNightshadeSetupExt; @@ -200,7 +201,7 @@ fn get_status_of_tx_hash_collision_for_implicit_account( /// Test that duplicate transactions from NEAR-implicit accounts are properly rejected. #[test] -fn test_transaction_hash_collision_for_implicit_account_fail() { +fn test_transaction_hash_collision_for_near_implicit_account_fail() { let protocol_version = ProtocolFeature::AccessKeyNonceForImplicitAccounts.protocol_version(); let secret_key = SecretKey::from_seed(KeyType::ED25519, "test"); let implicit_account_id = derive_account_id_from_public_key(&secret_key.public_key()); @@ -216,7 +217,7 @@ fn test_transaction_hash_collision_for_implicit_account_fail() { /// Test that duplicate transactions from NEAR-implicit accounts are not rejected until protocol upgrade. #[test] -fn test_transaction_hash_collision_for_implicit_account_ok() { +fn test_transaction_hash_collision_for_near_implicit_account_ok() { let protocol_version = ProtocolFeature::AccessKeyNonceForImplicitAccounts.protocol_version() - 1; let secret_key = SecretKey::from_seed(KeyType::ED25519, "test"); @@ -231,6 +232,103 @@ fn test_transaction_hash_collision_for_implicit_account_ok() { ); } +/// Test that transactions from ETH-implicit accounts are rejected. +#[test] +fn test_transaction_from_eth_implicit_account_fail() { + if !checked_feature!("stable", EthImplicit, PROTOCOL_VERSION) { + return; + } + let genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); + let mut env = TestEnv::builder(ChainGenesis::test()) + .real_epoch_managers(&genesis.config) + .nightshade_runtimes(&genesis) + .build(); + let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap(); + let deposit_for_account_creation = 10u128.pow(23); + let mut height = 1; + let blocks_number = 5; + let signer1 = InMemorySigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1"); + + let secret_key = SecretKey::from_seed(KeyType::SECP256K1, "test"); + let public_key = secret_key.public_key(); + let implicit_account_id = derive_account_id_from_public_key(&public_key); + let implicit_account_signer = + InMemorySigner::from_secret_key(implicit_account_id.clone(), secret_key); + + // Send money to ETH-implicit account, invoking its creation. + let send_money_tx = SignedTransaction::send_money( + 1, + "test1".parse().unwrap(), + implicit_account_id.clone(), + &signer1, + deposit_for_account_creation, + *genesis_block.hash(), + ); + // Check for tx success status and get new block height. + height = check_tx_processing(&mut env, send_money_tx, height, blocks_number); + let block = env.clients[0].chain.get_block_by_height(height - 1).unwrap(); + + // Try to send money from ETH-implicit account using `(block_height - 1) * 1e6` as a nonce. + // That would be a good nonce for any access key, but the transaction should fail nonetheless because there is no access key. + let nonce = (height - 1) * AccessKey::ACCESS_KEY_NONCE_RANGE_MULTIPLIER; + let send_money_from_implicit_account_tx = SignedTransaction::send_money( + nonce, + implicit_account_id.clone(), + "test0".parse().unwrap(), + &implicit_account_signer, + 100, + *block.hash(), + ); + let response = env.clients[0].process_tx(send_money_from_implicit_account_tx, false, false); + let expected_tx_error = ProcessTxResponse::InvalidTx(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: implicit_account_id.clone(), + public_key: public_key.clone(), + }, + )); + assert_eq!(response, expected_tx_error); + + // Try to delete ETH-implicit account. + let delete_implicit_account_tx = SignedTransaction::delete_account( + nonce, + implicit_account_id.clone(), + implicit_account_id.clone(), + "test0".parse().unwrap(), + &implicit_account_signer, + *block.hash(), + ); + let response = env.clients[0].process_tx(delete_implicit_account_tx, false, false); + assert_eq!(response, expected_tx_error); + + // Try to add an access key to the ETH-implicit account. + let add_access_key_to_implicit_account_tx = SignedTransaction::from_actions( + nonce, + implicit_account_id.clone(), + implicit_account_id.clone(), + &implicit_account_signer, + vec![Action::AddKey(Box::new(AddKeyAction { + public_key, + access_key: AccessKey::full_access(), + }))], + *block.hash(), + ); + let response = env.clients[0].process_tx(add_access_key_to_implicit_account_tx, false, false); + assert_eq!(response, expected_tx_error); + + // Try to deploy the Wallet Contract again to the ETH-implicit account. + let wallet_contract_code = wallet_contract_placeholder().code().to_vec(); + let add_access_key_to_implicit_account_tx = SignedTransaction::from_actions( + nonce, + implicit_account_id.clone(), + implicit_account_id, + &implicit_account_signer, + vec![Action::DeployContract(DeployContractAction { code: wallet_contract_code })], + *block.hash(), + ); + let response = env.clients[0].process_tx(add_access_key_to_implicit_account_tx, false, false); + assert_eq!(response, expected_tx_error); +} + /// Test that chunks with transactions that have expired are considered invalid. #[test] fn test_chunk_transaction_validity() { diff --git a/integration-tests/src/tests/client/features/delegate_action.rs b/integration-tests/src/tests/client/features/delegate_action.rs index 503b6278541..f8234b8def7 100644 --- a/integration-tests/src/tests/client/features/delegate_action.rs +++ b/integration-tests/src/tests/client/features/delegate_action.rs @@ -12,18 +12,21 @@ use near_crypto::{KeyType, PublicKey, Signer}; use near_primitives::account::{ id::AccountType, AccessKey, AccessKeyPermission, FunctionCallPermission, }; +use near_primitives::checked_feature; use near_primitives::config::ActionCosts; use near_primitives::errors::{ ActionError, ActionErrorKind, ActionsValidationError, InvalidAccessKeyError, InvalidTxError, TxExecutionError, }; -use near_primitives::test_utils::{create_user_test_signer, near_implicit_test_account}; +use near_primitives::test_utils::{ + create_user_test_signer, eth_implicit_test_account, near_implicit_test_account, +}; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; use near_primitives::types::{AccountId, Balance}; -use near_primitives::version::{ProtocolFeature, ProtocolVersion}; +use near_primitives::version::{ProtocolFeature, ProtocolVersion, PROTOCOL_VERSION}; use near_primitives::views::{ AccessKeyPermissionView, ExecutionStatusView, FinalExecutionOutcomeView, FinalExecutionStatus, }; @@ -121,6 +124,7 @@ fn check_meta_tx_execution( receiver: AccountId, ) -> (FinalExecutionOutcomeView, i128, i128, i128) { let node_user = node.user(); + let protocol_version = node.genesis().config.protocol_version; assert_eq!( relayer, @@ -137,7 +141,16 @@ fn check_meta_tx_execution( .nonce; let user_pubk = match sender.get_account_type() { AccountType::NearImplicitAccount => PublicKey::from_near_implicit_account(&sender).unwrap(), - AccountType::EthImplicitAccount => PublicKey::from_seed(KeyType::ED25519, sender.as_ref()), + AccountType::EthImplicitAccount => { + if checked_feature!("stable", EthImplicit, protocol_version) { + // Eth-implicit accounts must not have access key. + assert!(node_user.is_locked(&sender).unwrap()); + // Panicking because no transaction can be made from this account (no access keys). + panic!("No access keys"); + } else { + PublicKey::from_seed(KeyType::ED25519, sender.as_ref()) + } + } AccountType::NamedAccount => PublicKey::from_seed(KeyType::ED25519, sender.as_ref()), }; let user_nonce_before = node_user.get_access_key(&sender, &user_pubk).unwrap().nonce; @@ -803,12 +816,22 @@ fn meta_tx_create_near_implicit_account_fails() { meta_tx_create_implicit_account_fails(near_implicit_test_account()); } +#[test] +fn meta_tx_create_eth_implicit_account_fails() { + if !checked_feature!("stable", EthImplicit, PROTOCOL_VERSION) { + return; + } + meta_tx_create_implicit_account_fails(eth_implicit_test_account()); +} + /// 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. +/// In case of Near-implicit accounts it only works with accounts that already exist +/// because it needs to do a nonce check against the access key, +/// which can only exist if the account exists. +/// In case of Eth-implicit accounts the access key does not exist anyway. fn meta_tx_create_and_use_implicit_account(new_account: AccountId) { let relayer = bob_account(); let sender = alice_account(); @@ -840,12 +863,24 @@ fn meta_tx_create_and_use_near_implicit_account() { meta_tx_create_and_use_implicit_account(near_implicit_test_account()); } -/// Creating an implicit account with a meta tx transfer and use the account in +#[test] +fn meta_tx_create_and_use_eth_implicit_account() { + if !checked_feature!("stable", EthImplicit, PROTOCOL_VERSION) { + return; + } + meta_tx_create_and_use_implicit_account(eth_implicit_test_account()); +} + +/// Creating an implicit account with a meta tx transfer and try using 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. +/// +/// Using the account should only work for Near-implicit accounts, +/// as Eth-implicit accounts do not have access keys +/// and they can only be used by calling associated smart contract. fn meta_tx_create_implicit_account(new_account: AccountId) { let relayer = bob_account(); let sender = alice_account(); @@ -860,7 +895,7 @@ fn meta_tx_create_implicit_account(new_account: AccountId) { let tx_cost = match new_account.get_account_type() { AccountType::NearImplicitAccount => fee_helper.create_account_transfer_full_key_cost(), - AccountType::EthImplicitAccount => panic!("must be near-implicit"), + AccountType::EthImplicitAccount => fee_helper.create_account_transfer_cost(), AccountType::NamedAccount => panic!("must be near-implicit"), }; check_meta_tx_no_fn_call( @@ -882,7 +917,7 @@ fn meta_tx_create_implicit_account(new_account: AccountId) { 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( + let tx_outcome = check_meta_tx_no_fn_call( &node, actions, tx_cost, @@ -890,16 +925,30 @@ fn meta_tx_create_implicit_account(new_account: AccountId) { 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); + ); + match new_account.get_account_type() { + AccountType::NearImplicitAccount => { + tx_outcome.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); + } + AccountType::EthImplicitAccount => tx_outcome.assert_success(), + AccountType::NamedAccount => panic!("must be near-implicit"), + }; } #[test] fn meta_tx_create_near_implicit_account() { meta_tx_create_implicit_account(near_implicit_test_account()); } + +#[test] +#[should_panic(expected = "No access keys")] +fn meta_tx_create_eth_implicit_account() { + if !checked_feature!("stable", EthImplicit, PROTOCOL_VERSION) { + return; + } + meta_tx_create_implicit_account(eth_implicit_test_account()); +} diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index d744243043d..e1adb87cc12 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -371,7 +371,7 @@ pub fn transfer_tokens_implicit_account(node: impl Node, public_key: PublicKey) } AccountType::EthImplicitAccount => { // A transfer to ETH-implicit address does not create access key. - assert!(node_user.get_access_key(&receiver_id, &public_key).is_err()); + assert!(view_access_key.is_err()); } AccountType::NamedAccount => std::panic!("must be implicit"), } diff --git a/integration-tests/src/user/mod.rs b/integration-tests/src/user/mod.rs index cc1da5b1a8f..0a3aa6bce4b 100644 --- a/integration-tests/src/user/mod.rs +++ b/integration-tests/src/user/mod.rs @@ -38,6 +38,9 @@ pub trait User { fn view_state(&self, account_id: &AccountId, prefix: &[u8]) -> Result; + /// Returns whether the account is locked (has no access keys). + fn is_locked(&self, account_id: &AccountId) -> Result; + fn view_call( &self, account_id: &AccountId, diff --git a/integration-tests/src/user/rpc_user.rs b/integration-tests/src/user/rpc_user.rs index 0e5e53c6cd2..012d624a9e0 100644 --- a/integration-tests/src/user/rpc_user.rs +++ b/integration-tests/src/user/rpc_user.rs @@ -91,6 +91,16 @@ impl User for RpcUser { } } + fn is_locked(&self, account_id: &AccountId) -> Result { + let query = QueryRequest::ViewAccessKeyList { account_id: account_id.clone() }; + match self.query(query)?.kind { + near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKeyList( + access_keys, + ) => Ok(access_keys.keys.is_empty()), + _ => Err("Invalid type of response".into()), + } + } + fn view_contract_code(&self, account_id: &AccountId) -> Result { let query = QueryRequest::ViewCode { account_id: account_id.clone() }; match self.query(query)?.kind { diff --git a/integration-tests/src/user/runtime_user.rs b/integration-tests/src/user/runtime_user.rs index 72f5c98d6c8..f450e64dc67 100644 --- a/integration-tests/src/user/runtime_user.rs +++ b/integration-tests/src/user/runtime_user.rs @@ -252,6 +252,14 @@ impl User for RuntimeUser { .map_err(|err| err.to_string()) } + fn is_locked(&self, account_id: &AccountId) -> Result { + let state_update = self.client.read().expect(POISONED_LOCK_ERR).get_state_update(); + self.trie_viewer + .view_access_keys(&state_update, account_id) + .map(|access_keys| access_keys.is_empty()) + .map_err(|err| err.to_string()) + } + fn view_call( &self, account_id: &AccountId,