diff --git a/Cargo.lock b/Cargo.lock index 9132efb9441..63c43ae5841 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4119,6 +4119,7 @@ dependencies = [ "serde_json", "serde_with", "serde_yaml", + "sha3", "smart-default", "strum", "thiserror", diff --git a/core/account-id/src/lib.rs b/core/account-id/src/lib.rs index 07a867e3427..c7c386c1c6c 100644 --- a/core/account-id/src/lib.rs +++ b/core/account-id/src/lib.rs @@ -79,8 +79,7 @@ impl AccountType { pub fn is_implicit(&self) -> bool { match &self { Self::NearImplicitAccount => true, - // TODO(eth-implicit) change to true later, see https://github.com/near/nearcore/issues/10018 - Self::EthImplicitAccount => false, + Self::EthImplicitAccount => true, Self::NamedAccount => false, } } 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-core/src/runtime/fees.rs b/core/primitives-core/src/runtime/fees.rs index 57ec33c4a64..56a77c9c287 100644 --- a/core/primitives-core/src/runtime/fees.rs +++ b/core/primitives-core/src/runtime/fees.rs @@ -217,8 +217,11 @@ pub fn transfer_exec_fee( (_, AccountType::NamedAccount) => transfer_fee, // No account will be created, just a regular transfer. (false, _) => transfer_fee, - // Currently, no account is created on transfer to ETH-implicit account, just a regular transfer. - (true, AccountType::EthImplicitAccount) => transfer_fee, + // Extra fee for the CreateAccount. + (true, AccountType::EthImplicitAccount) => { + transfer_fee + + cfg.fee(ActionCosts::create_account).exec_fee() + } // Extra fees for the CreateAccount and AddFullAccessKey. (true, AccountType::NearImplicitAccount) => { transfer_fee @@ -235,23 +238,21 @@ pub fn transfer_send_fee( receiver_account_type: AccountType, ) -> Gas { let transfer_fee = cfg.fee(ActionCosts::transfer).send_fee(sender_is_receiver); - match (implicit_account_creation_allowed, receiver_account_type) { // Regular transfer to a named account. (_, AccountType::NamedAccount) => transfer_fee, // No account will be created, just a regular transfer. (false, _) => transfer_fee, - // Currently, no account is created on transfer to ETH-implicit account, just a regular transfer. - (true, AccountType::EthImplicitAccount) => transfer_fee, + // Extra fee for the CreateAccount. + (true, AccountType::EthImplicitAccount) => { + transfer_fee + + cfg.fee(ActionCosts::create_account).send_fee(sender_is_receiver) + } // Extra fees for the CreateAccount and AddFullAccessKey. (true, AccountType::NearImplicitAccount) => { -<<<<<<< HEAD transfer_fee + cfg.fee(ActionCosts::create_account).send_fee(sender_is_receiver) + cfg.fee(ActionCosts::add_full_access_key).send_fee(sender_is_receiver) -======= - transfer_fee + create_account_fee + add_access_key_fee ->>>>>>> a96103cbc (Style fix for cargo fmt check) } } } diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 4334d4f3447..77472213c04 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -29,6 +29,7 @@ serde.workspace = true serde_json.workspace = true serde_with.workspace = true serde_yaml.workspace = true +sha3.workspace = true smart-default.workspace = true stdx.workspace = true strum.workspace = true diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 16ba9beee1c..c9cdabed6c0 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -209,6 +209,8 @@ pub enum InvalidAccessKeyError { }, /// Having a deposit with a function call action is not allowed with a function call access key. DepositWithFunctionCall, + /// ETH-implicit `account_id` isn't derived from the `public_key`. + InvalidPkForEthAddress { account_id: AccountId, public_key: PublicKey }, } /// Describes the error for validating a list of actions. @@ -485,8 +487,8 @@ pub enum ActionErrorKind { /// receipt validation. NewReceiptValidationError(ReceiptValidationError), /// Error occurs when a `CreateAccount` action is called on hex-characters - /// account of length 64. See implicit account creation NEP: - /// . + /// account of length 64 or 42 (when starting with '0x'). + /// See implicit account creation NEP: . /// /// TODO(#8598): This error is named very poorly. A better name would be /// `OnlyNamedAccountCreationAllowed`. @@ -609,7 +611,12 @@ impl Display for InvalidAccessKeyError { ), InvalidAccessKeyError::DepositWithFunctionCall => { write!(f, "Having a deposit with a function call action is not allowed with a function call access key.") - } + }, + InvalidAccessKeyError::InvalidPkForEthAddress { account_id, public_key } => write!( + f, + "ETH-implicit address {:?} isn't derived from the public_key {}", + account_id, public_key + ), } } } diff --git a/core/primitives/src/utils.rs b/core/primitives/src/utils.rs index 06894bb9eaf..20824baeff0 100644 --- a/core/primitives/src/utils.rs +++ b/core/primitives/src/utils.rs @@ -17,7 +17,7 @@ use crate::version::{ CREATE_RECEIPT_ID_SWITCH_TO_CURRENT_BLOCK_VERSION, }; -use near_crypto::ED25519PublicKey; +use near_crypto::{KeyType, PublicKey}; use near_primitives_core::account::id::AccountId; use std::mem::size_of; @@ -471,24 +471,40 @@ where /// Derives `AccountId` from `PublicKey``. /// If the key type is ED25519, returns hex-encoded copy of the key. -pub fn derive_near_implicit_account_id(public_key: &ED25519PublicKey) -> AccountId { - hex::encode(public_key).parse().unwrap() +/// 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() { + KeyType::ED25519 => { + hex::encode(public_key.key_data()).parse().unwrap() + }, + KeyType::SECP256K1 => { + use sha3::Digest; + let pk_hash = sha3::Keccak256::digest(&public_key.key_data()); + format!("0x{}", hex::encode(&pk_hash[12..32])).parse().unwrap() + }, + } } #[cfg(test)] mod tests { use super::*; - use near_crypto::{KeyType, PublicKey}; #[test] fn test_derive_account_id_from_ed25519_public_key() { let public_key = PublicKey::from_seed(KeyType::ED25519, "test"); let expected: AccountId = "bb4dc639b212e075a751685b26bdcea5920a504181ff2910e8549742127092a0".parse().unwrap(); - let account_id = derive_near_implicit_account_id(public_key.unwrap_as_ed25519()); + let account_id = derive_account_id_from_public_key(&public_key); assert_eq!(account_id, expected); } + #[test] + fn test_derive_account_id_from_secp256k1_public_key() { + let public_key = PublicKey::from_seed(KeyType::SECP256K1, "test"); + let expected: AccountId = "0x96791e923f8cf697ad9c3290f2c9059f0231b24c".parse().unwrap(); + assert_eq!(derive_account_id_from_public_key(&public_key), expected); + } + #[test] fn test_num_chunk_producers() { for num_seats in 1..50 { 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 bb07c943c2b..bd482bdc79b 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 @@ -18,7 +18,7 @@ use near_primitives::shard_layout::ShardLayout; use near_primitives::sharding::ChunkHash; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, BlockHeight}; -use near_primitives::utils::derive_near_implicit_account_id; +use near_primitives::utils::derive_account_id_from_public_key; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives::views::FinalExecutionStatus; use nearcore::config::GenesisExt; @@ -200,11 +200,11 @@ 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_near_implicit_account_id(secret_key.public_key().unwrap_as_ed25519()); + derive_account_id_from_public_key(&secret_key.public_key()); let implicit_account_signer = InMemorySigner::from_secret_key(implicit_account_id, secret_key); assert_matches!( get_status_of_tx_hash_collision_for_implicit_account( @@ -215,14 +215,45 @@ fn test_transaction_hash_collision_for_implicit_account_fail() { ); } +/// Test that duplicate transactions from ETH-implicit accounts are properly rejected. +#[test] +fn test_transaction_hash_collision_for_eth_implicit_account_fail() { + let protocol_version = ProtocolFeature::AccessKeyNonceForImplicitAccounts.protocol_version(); + let secret_key = SecretKey::from_seed(KeyType::SECP256K1, "test"); + let implicit_account_id = derive_account_id_from_public_key(&secret_key.public_key()); + let implicit_account_signer = InMemorySigner::from_secret_key(implicit_account_id, secret_key); + assert_matches!( + get_status_of_tx_hash_collision_for_implicit_account(protocol_version, implicit_account_signer), + ProcessTxResponse::InvalidTx(InvalidTxError::InvalidNonce { .. }) + ); +} + /// 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"); let implicit_account_id = - derive_near_implicit_account_id(secret_key.public_key().unwrap_as_ed25519()); + derive_account_id_from_public_key(&secret_key.public_key()); + let implicit_account_signer = InMemorySigner::from_secret_key(implicit_account_id, secret_key); + assert_matches!( + get_status_of_tx_hash_collision_for_implicit_account( + protocol_version, + implicit_account_signer + ), + ProcessTxResponse::ValidTx + ); +} + +/// Test that duplicate transactions from ETH-implicit accounts are not rejected until protocol upgrade. +#[test] +fn test_transaction_hash_collision_for_eth_implicit_account_ok() { + let protocol_version = + ProtocolFeature::AccessKeyNonceForImplicitAccounts.protocol_version() - 1; + let secret_key = SecretKey::from_seed(KeyType::SECP256K1, "test"); + let implicit_account_id = + derive_account_id_from_public_key(&secret_key.public_key()); let implicit_account_signer = InMemorySigner::from_secret_key(implicit_account_id, secret_key); assert_matches!( get_status_of_tx_hash_collision_for_implicit_account( diff --git a/integration-tests/src/tests/client/features/delegate_action.rs b/integration-tests/src/tests/client/features/delegate_action.rs index c95e7381bec..d4f335e3b93 100644 --- a/integration-tests/src/tests/client/features/delegate_action.rs +++ b/integration-tests/src/tests/client/features/delegate_action.rs @@ -17,7 +17,8 @@ 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, @@ -135,12 +136,17 @@ fn check_meta_tx_execution( .get_access_key(&relayer, &PublicKey::from_seed(KeyType::ED25519, &relayer)) .unwrap() .nonce; + let user_pubk = match sender.get_account_type() { - AccountType::NearImplicitAccount => PublicKey::from_near_implicit_account(&sender).unwrap(), - AccountType::EthImplicitAccount => PublicKey::from_seed(KeyType::SECP256K1, &sender), - AccountType::NamedAccount => PublicKey::from_seed(KeyType::ED25519, &sender), + AccountType::NearImplicitAccount => Some(PublicKey::from_near_implicit_account(&sender).unwrap()), + // Cannot infer public key from ETH-implicit address, because ETH-implicit address itself is inferred from the public key. + // However, it is for testing purposes only (checking nonce, that might not exist for ETH-implicit account anyway). + AccountType::EthImplicitAccount => None, + AccountType::NamedAccount => Some(PublicKey::from_seed(KeyType::ED25519, &sender)), }; - let user_nonce_before = node_user.get_access_key(&sender, &user_pubk).unwrap().nonce; + let user_nonce_before = user_pubk.map(|user_pubk| + node_user.get_access_key(&sender, &user_pubk).unwrap().nonce + ); let tx_result = node_user.meta_tx(sender.clone(), receiver.clone(), relayer.clone(), actions).unwrap(); @@ -154,12 +160,15 @@ fn check_meta_tx_execution( .unwrap() .nonce; assert_eq!(relayer_nonce, relayer_nonce_before + 1); - // user key must be checked for existence (to test DeleteKey action) - if let Ok(user_nonce) = node_user - .get_access_key(&sender, &PublicKey::from_seed(KeyType::ED25519, &sender)) - .map(|key| key.nonce) - { - assert_eq!(user_nonce, user_nonce_before + 1); + + if let Some(user_nonce_before) = user_nonce_before { + // user key must be checked for existence (to test DeleteKey action) + if let Ok(user_nonce) = node_user + .get_access_key(&sender, &PublicKey::from_seed(KeyType::ED25519, &sender)) + .map(|key| key.nonce) + { + assert_eq!(user_nonce, user_nonce_before + 1); + } } let sender_after = node_user.view_balance(&sender).unwrap_or(0); @@ -803,6 +812,11 @@ 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() { + 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. /// @@ -840,6 +854,11 @@ fn meta_tx_create_and_use_near_implicit_account() { meta_tx_create_and_use_implicit_account(near_implicit_test_account()); } +#[test] +fn meta_tx_create_and_use_eth_implicit_account() { + meta_tx_create_and_use_implicit_account(eth_implicit_test_account()); +} + /// Creating an implicit account with a meta tx transfer and use the account in /// a second meta transaction. /// @@ -860,8 +879,8 @@ 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::NamedAccount => panic!("must be near-implicit"), + AccountType::EthImplicitAccount => fee_helper.create_account_transfer_cost(), + AccountType::NamedAccount => panic!("must be implicit"), }; check_meta_tx_no_fn_call( &node, @@ -903,3 +922,8 @@ fn meta_tx_create_implicit_account(new_account: AccountId) { fn meta_tx_create_near_implicit_account() { meta_tx_create_implicit_account(near_implicit_test_account()); } + +#[test] +fn meta_tx_create_eth_implicit_account() { + meta_tx_create_implicit_account(eth_implicit_test_account()); +} diff --git a/integration-tests/src/tests/client/features/restrict_tla.rs b/integration-tests/src/tests/client/features/restrict_tla.rs index 250a2e42af1..b50675845ec 100644 --- a/integration-tests/src/tests/client/features/restrict_tla.rs +++ b/integration-tests/src/tests/client/features/restrict_tla.rs @@ -23,11 +23,11 @@ fn test_create_top_level_accounts() { .build(); // These accounts cannot be created because they are top level accounts that are not implicit. - // Note that implicit accounts have to be 64 characters long. + // Note that implicit accounts have to be 64 or 42 (if starts with '0x') characters long. let top_level_accounts = [ - "0x06012c8cf97bead5deae237070f9587f8e7a266d", - "0x5e97870f263700f46aa00d967821199b9bc5a120", - "0x0000000000000000000000000000000000000000", + "0x06012c8cf97bead5deae237070f9587f8e7a266da", + "0a5e97870f263700f46aa00d967821199b9bc5a120", + "0x000000000000000000000000000000000000000", "alice", "thisisaveryverylongtoplevelaccount", ]; diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 4a47ec2c397..9db5adbfad2 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -16,7 +16,7 @@ use near_primitives::errors::{ }; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::types::{AccountId, Balance, TrieNodesCount}; -use near_primitives::utils::derive_near_implicit_account_id; +use near_primitives::utils::derive_account_id_from_public_key; use near_primitives::views::{ AccessKeyView, AccountView, ExecutionMetadataView, FinalExecutionOutcomeView, FinalExecutionStatus, @@ -336,12 +336,12 @@ pub fn transfer_tokens_implicit_account(node: impl Node, public_key: PublicKey) let root = node_user.get_state_root(); let tokens_used = 10u128.pow(25); let fee_helper = fee_helper(&node); - let receiver_id = derive_near_implicit_account_id(public_key.unwrap_as_ed25519()); + let receiver_id = derive_account_id_from_public_key(&public_key); let transfer_cost = match receiver_id.get_account_type() { AccountType::NearImplicitAccount => fee_helper.create_account_transfer_full_key_cost(), - AccountType::EthImplicitAccount => std::panic!("must be near-implicit"), - AccountType::NamedAccount => std::panic!("must be near-implicit"), + AccountType::EthImplicitAccount => fee_helper.create_account_transfer_cost(), + AccountType::NamedAccount => std::panic!("must be implicit"), }; let transaction_result = @@ -369,8 +369,10 @@ pub fn transfer_tokens_implicit_account(node: impl Node, public_key: PublicKey) AccountType::NearImplicitAccount => { assert_eq!(view_access_key.unwrap(), AccessKey::full_access().into()); } - AccountType::EthImplicitAccount => std::panic!("must be near-implicit"), - AccountType::NamedAccount => std::panic!("must be near-implicit"), + AccountType::EthImplicitAccount => { + assert!(node_user.get_access_key(&receiver_id, &public_key).is_err()); + } + AccountType::NamedAccount => std::panic!("must be implicit"), } let transaction_result = @@ -401,7 +403,7 @@ pub fn trying_to_create_implicit_account(node: impl Node, public_key: PublicKey) let root = node_user.get_state_root(); let tokens_used = 10u128.pow(25); let fee_helper = fee_helper(&node); - let receiver_id = derive_near_implicit_account_id(public_key.unwrap_as_ed25519()); + let receiver_id = derive_account_id_from_public_key(&public_key); let transaction_result = node_user .create_account( @@ -412,20 +414,23 @@ pub fn trying_to_create_implicit_account(node: impl Node, public_key: PublicKey) ) .unwrap(); + let fail_cost = + fee_helper.create_account_transfer_full_key_cost_fail_on_create_account(); + let create_account_fee = + fee_helper.cfg().fee(ActionCosts::create_account).send_fee(false); + let add_access_key_fee = fee_helper + .cfg() + .fee(near_primitives::config::ActionCosts::add_full_access_key) + .send_fee(false); + let cost = match receiver_id.get_account_type() { AccountType::NearImplicitAccount => { - let fail_cost = - fee_helper.create_account_transfer_full_key_cost_fail_on_create_account(); - let create_account_fee = - fee_helper.cfg().fee(ActionCosts::create_account).send_fee(false); - let add_access_key_fee = fee_helper - .cfg() - .fee(near_primitives::config::ActionCosts::add_full_access_key) - .send_fee(false); fail_cost + fee_helper.gas_to_balance(create_account_fee + add_access_key_fee) } - AccountType::EthImplicitAccount => std::panic!("must be near-implicit"), - AccountType::NamedAccount => std::panic!("must be near-implicit"), + AccountType::EthImplicitAccount => { + fail_cost + fee_helper.gas_to_balance(create_account_fee) + } + AccountType::NamedAccount => std::panic!("must be implicit"), }; assert_eq!( diff --git a/integration-tests/src/tests/standard_cases/runtime.rs b/integration-tests/src/tests/standard_cases/runtime.rs index c30551085f0..5fe99b9d2e7 100644 --- a/integration-tests/src/tests/standard_cases/runtime.rs +++ b/integration-tests/src/tests/standard_cases/runtime.rs @@ -1,6 +1,7 @@ use crate::node::RuntimeNode; use crate::tests::standard_cases::*; use near_chain_configs::Genesis; +use near_crypto::SecretKey; use near_primitives::state_record::StateRecord; use nearcore::config::{GenesisExt, TESTING_INIT_BALANCE}; use testlib::runtime_utils::{add_test_contract, alice_account, bob_account}; @@ -120,6 +121,13 @@ fn test_transfer_tokens_near_implicit_account_runtime() { transfer_tokens_implicit_account(node, public_key); } +#[test] +fn test_transfer_tokens_eth_implicit_account_runtime() { + let node = create_runtime_node(); + let secret_key = SecretKey::from_seed(KeyType::SECP256K1, "test"); + transfer_tokens_implicit_account(node, secret_key.public_key()); +} + #[test] fn test_trying_to_create_near_implicit_account_runtime() { let node = create_runtime_node(); @@ -127,6 +135,13 @@ fn test_trying_to_create_near_implicit_account_runtime() { trying_to_create_implicit_account(node, public_key); } +#[test] +fn test_trying_to_create_eth_implicit_account_runtime() { + let node = create_runtime_node(); + let secret_key = SecretKey::from_seed(KeyType::SECP256K1, "test"); + trying_to_create_implicit_account(node, secret_key.public_key()); +} + #[test] fn test_smart_contract_reward_runtime() { let node = create_runtime_node(); diff --git a/integration-tests/src/user/mod.rs b/integration-tests/src/user/mod.rs index cc1da5b1a8f..36c5ead218c 100644 --- a/integration-tests/src/user/mod.rs +++ b/integration-tests/src/user/mod.rs @@ -4,7 +4,7 @@ use futures::{future::LocalBoxFuture, FutureExt}; use near_crypto::{PublicKey, Signer}; use near_jsonrpc_primitives::errors::ServerError; -use near_primitives::account::AccessKey; +use near_primitives::account::{AccessKey, id::AccountType}; use near_primitives::action::delegate::{DelegateAction, NonDelegateAction, SignedDelegateAction}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; @@ -262,10 +262,19 @@ pub trait User { actions: Vec, ) -> Result { let inner_signer = create_user_test_signer(&signer_id); - let user_nonce = self - .get_access_key(&signer_id, &inner_signer.public_key) - .expect("failed reading user's nonce for access key") - .nonce; + let access_key = self.get_access_key(&signer_id, &inner_signer.public_key); + + let user_nonce = match access_key { + Ok(access_key) => access_key.nonce, + Err(_) => match signer_id.get_account_type() { + AccountType::EthImplicitAccount => { + // TODO, use block_height + 0 + } + _ => panic!("failed reading user's nonce for access key"), + }, + }; + let delegate_action = DelegateAction { sender_id: signer_id.clone(), receiver_id, diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index cc4b60eceae..af8deba4942 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -473,9 +473,16 @@ pub(crate) fn action_implicit_account_creation_transfer( } // Invariant: The `account_id` is implicit. // It holds because in the only calling site, we've checked the permissions before. - AccountType::EthImplicitAccount | AccountType::NamedAccount => { - panic!("must be near-implicit") + AccountType::EthImplicitAccount => { + *account = Some(Account::new( + transfer.deposit, + 0, + CryptoHash::default(), + fee_config.storage_usage_config.num_bytes_account + + fee_config.storage_usage_config.num_extra_bytes_record, + )); } + AccountType::NamedAccount => panic!("must be implicit") } } diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 08323a15948..d5681decd9f 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -2,7 +2,7 @@ use crate::config::{total_prepaid_gas, tx_cost, TransactionCost}; use crate::near_primitives::account::Account; use crate::VerificationResult; use near_crypto::key_conversion::is_valid_staking_key; -use near_primitives::account::AccessKeyPermission; +use near_primitives::account::{AccessKey, AccessKeyPermission}; use near_primitives::action::delegate::SignedDelegateAction; use near_primitives::checked_feature; use near_primitives::errors::{ @@ -17,8 +17,10 @@ use near_primitives::transaction::{ }; use near_primitives::types::{AccountId, Balance}; use near_primitives::types::{BlockHeight, StorageUsage}; +use near_primitives::utils::derive_account_id_from_public_key; use near_primitives::version::ProtocolFeature; use near_primitives::version::ProtocolVersion; +use near_primitives_core::account::id::AccountType; use near_store::{ get_access_key, get_account, set_access_key, set_account, StorageError, TrieUpdate, }; @@ -144,6 +146,7 @@ pub fn verify_and_charge_transaction( )?; let transaction = &signed_transaction.transaction; let signer_id = &transaction.signer_id; + let public_key = &transaction.public_key; let mut signer = match get_account(state_update, signer_id)? { Some(signer) => signer, @@ -151,26 +154,42 @@ pub fn verify_and_charge_transaction( return Err(InvalidTxError::SignerDoesNotExist { signer_id: signer_id.clone() }.into()); } }; - let mut access_key = match get_access_key(state_update, signer_id, &transaction.public_key)? { - Some(access_key) => access_key, - None => { - return Err(InvalidTxError::InvalidAccessKeyError( - InvalidAccessKeyError::AccessKeyNotFound { - account_id: signer_id.clone(), - public_key: transaction.public_key.clone(), - }, - ) - .into()); + let mut access_key = match get_access_key(state_update, signer_id, public_key)? { + Some(access_key) => { + if transaction.nonce <= access_key.nonce { + return Err(InvalidTxError::InvalidNonce { + tx_nonce: transaction.nonce, + ak_nonce: access_key.nonce, + } + .into()); + } + access_key + } + None => match signer_id.get_account_type() { + AccountType::EthImplicitAccount => { + if derive_account_id_from_public_key(public_key) == *signer_id { + // TODO What about increasing storage usage for that account, because we added access key? + AccessKey::full_access() + } else { + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::InvalidPkForEthAddress { + account_id: signer_id.clone(), + public_key: public_key.clone(), + }) + .into()); + } + }, + _ => { + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: signer_id.clone(), + public_key: public_key.clone(), + }) + .into()); + }, } }; - if transaction.nonce <= access_key.nonce { - return Err(InvalidTxError::InvalidNonce { - tx_nonce: transaction.nonce, - ak_nonce: access_key.nonce, - } - .into()); - } if checked_feature!("stable", AccessKeyNonceRange, current_protocol_version) { if let Some(height) = block_height { let upper_bound = @@ -202,7 +221,7 @@ pub fn verify_and_charge_transaction( *allowance = allowance.checked_sub(total_cost).ok_or_else(|| { InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::NotEnoughAllowance { account_id: signer_id.clone(), - public_key: transaction.public_key.clone(), + public_key: public_key.clone(), allowance: *allowance, cost: total_cost, }) @@ -268,7 +287,7 @@ pub fn verify_and_charge_transaction( } }; - set_access_key(state_update, signer_id.clone(), transaction.public_key.clone(), &access_key); + set_access_key(state_update, signer_id.clone(), public_key.clone(), &access_key); set_account(state_update, signer_id.clone(), &signer); Ok(VerificationResult { gas_burnt, gas_remaining, receipt_gas_price, burnt_amount }) diff --git a/test-utils/runtime-tester/src/fuzzing.rs b/test-utils/runtime-tester/src/fuzzing.rs index c13c29851be..d2ea8a69b9a 100644 --- a/test-utils/runtime-tester/src/fuzzing.rs +++ b/test-utils/runtime-tester/src/fuzzing.rs @@ -508,7 +508,12 @@ impl Scope { for x in b'0'..=b'9' { chars.push(x); } - for _ in 0..64 { + let is_eth_implicit = u.arbitrary::(); + let len: u32 = if is_eth_implicit? { 40 } else { 64 }; + if is_eth_implicit? { + new_id_vec.extend_from_slice(b"0x"); + } + for _ in 0..len { new_id_vec.push(*u.choose(&chars)?); } let new_id = String::from_utf8(new_id_vec).unwrap(); diff --git a/test-utils/testlib/src/fees_utils.rs b/test-utils/testlib/src/fees_utils.rs index ddd964946a5..f07ad46db69 100644 --- a/test-utils/testlib/src/fees_utils.rs +++ b/test-utils/testlib/src/fees_utils.rs @@ -56,10 +56,24 @@ impl FeeHelper { exec_gas + send_gas } + pub fn create_account_transfer_fee(&self) -> Gas { + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee() + + self.cfg().fee(ActionCosts::transfer).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false); + exec_gas + send_gas + } + pub fn create_account_transfer_full_key_cost(&self) -> Balance { self.gas_to_balance(self.create_account_transfer_full_key_fee()) } + pub fn create_account_transfer_cost(&self) -> Balance { + self.gas_to_balance(self.create_account_transfer_fee()) + } + pub fn create_account_transfer_full_key_cost_no_reward(&self) -> Balance { let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + self.cfg().fee(ActionCosts::create_account).exec_fee() @@ -82,6 +96,15 @@ impl FeeHelper { self.gas_to_balance(exec_gas + send_gas) } + pub fn create_account_transfer_cost_fail_on_create_account(&self) -> Balance { + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + + self.cfg().fee(ActionCosts::create_account).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false) + + self.cfg().fee(ActionCosts::create_account).send_fee(false) + + self.cfg().fee(ActionCosts::transfer).send_fee(false); + self.gas_to_balance(exec_gas + send_gas) + } + pub fn deploy_contract_cost(&self, num_bytes: u64) -> Balance { let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + self.cfg().fee(ActionCosts::deploy_contract_base).exec_fee() @@ -118,10 +141,6 @@ impl FeeHelper { self.gas_to_balance(self.transfer_fee()) } - pub fn transfer_cost_64len_hex(&self) -> Balance { - self.create_account_transfer_full_key_cost() - } - pub fn stake_cost(&self) -> Balance { let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + self.cfg().fee(ActionCosts::stake).exec_fee(); diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index ee5d617d7b1..075cc82c02d 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -492,7 +492,7 @@ impl ForkNetworkCommand { has_full_key.insert(account_id.clone()); } let new_account_id = map_account(&account_id, None); - let replacement = map_key(&public_key, None); + let replacement = map_key(&public_key, None, &account_id); storage_mutator.delete_access_key(account_id, public_key)?; storage_mutator.set_access_key( new_account_id, diff --git a/tools/mirror/src/genesis.rs b/tools/mirror/src/genesis.rs index 6f354d8df76..912b3d6c533 100644 --- a/tools/mirror/src/genesis.rs +++ b/tools/mirror/src/genesis.rs @@ -32,7 +32,7 @@ pub fn map_records>( near_chain_configs::stream_records_from_file(reader, |mut r| { match &mut r { StateRecord::AccessKey { account_id, public_key, access_key } => { - let replacement = crate::key_mapping::map_key(&public_key, secret.as_ref()); + let replacement = crate::key_mapping::map_key(&public_key, secret.as_ref(), account_id); let new_record = StateRecord::AccessKey { account_id: crate::key_mapping::map_account(&account_id, secret.as_ref()), public_key: replacement.public_key(), diff --git a/tools/mirror/src/key_mapping.rs b/tools/mirror/src/key_mapping.rs index 4cbc8608419..7c852dec5df 100644 --- a/tools/mirror/src/key_mapping.rs +++ b/tools/mirror/src/key_mapping.rs @@ -1,9 +1,10 @@ use hkdf::Hkdf; use near_crypto::{ED25519PublicKey, ED25519SecretKey, PublicKey, Secp256K1PublicKey, SecretKey}; -use near_primitives::types::AccountId; -use near_primitives::utils::derive_near_implicit_account_id; use near_primitives_core::account::id::AccountType; +use near_primitives::types::AccountId; +use near_primitives::utils::derive_account_id_from_public_key; use sha2::Sha256; +use std::fmt::Debug; // there is nothing special about this key, it's just some randomly generated one. // We will ensure that every account in the target chain has at least one full access @@ -48,11 +49,11 @@ fn map_ed25519( ED25519SecretKey(buf) } -fn secp256k1_from_slice(buf: &mut [u8], public: &Secp256K1PublicKey) -> secp256k1::SecretKey { +fn secp256k1_from_slice(buf: &mut [u8], mapped_from: &T) -> secp256k1::SecretKey { match secp256k1::SecretKey::from_slice(buf) { Ok(s) => s, Err(_) => { - tracing::warn!(target: "mirror", "Something super unlikely occurred! SECP256K1 key mapped from {:?} is too large. Flipping most significant bit.", public); + tracing::warn!(target: "mirror", "Something super unlikely occurred! SECP256K1 key mapped from {:?} is too large. Flipping most significant bit.", mapped_from); // If we got an error, it means that either `buf` is all zeros, or that when interpreted as a 256-bit // int, it is larger than the order of the secp256k1 curve. Since the order of the curve starts with 0xFF, // in either case flipping the first bit should work, and we can unwrap() below. @@ -67,7 +68,6 @@ fn map_secp256k1( secret: Option<&[u8; crate::secret::SECRET_LEN]>, ) -> secp256k1::SecretKey { let mut buf = [0; secp256k1::constants::SECRET_KEY_SIZE]; - match secret { Some(secret) => { let hk = Hkdf::::new(None, secret); @@ -77,25 +77,68 @@ fn map_secp256k1( buf.copy_from_slice(&public.as_ref()[..secp256k1::constants::SECRET_KEY_SIZE]); } }; - secp256k1_from_slice(&mut buf, public) } // This maps the public key to a secret key so that we can sign -// transactions on the target chain. If secret is None, then we just +// transactions on the target chain. If secret is None, then we just // use the bytes of the public key directly, otherwise we feed the // public key to a key derivation function. -pub fn map_key(key: &PublicKey, secret: Option<&[u8; crate::secret::SECRET_LEN]>) -> SecretKey { - match key { - PublicKey::ED25519(k) => SecretKey::ED25519(map_ed25519(k, secret)), - PublicKey::SECP256K1(k) => SecretKey::SECP256K1(map_secp256k1(k, secret)), +// If the `key` corresponds to a ETH-implicit `account_id`, +// then the new key will be derived from that ETH address. +pub fn map_key( + key: &PublicKey, + secret: Option<&[u8; crate::secret::SECRET_LEN]>, + account_id: &AccountId, +) -> SecretKey { + match account_id.get_account_type() { + AccountType::NearImplicitAccount => { + match key { + PublicKey::ED25519(k) => SecretKey::ED25519(map_ed25519(k, secret)), + // TODO Is it true? + _ => panic!("NEAR-implicit account can only have ED25519 access key added"), + } + }, + AccountType::EthImplicitAccount => { + match key { + PublicKey::SECP256K1(_) => map_secp256k1_from_eth_address(account_id, secret), + // TODO Is it true? + _ => panic!("ETH-implicit account can only have SECP256K1 access key added"), + } + }, + AccountType::NamedAccount => { + match key { + PublicKey::ED25519(k) => SecretKey::ED25519(map_ed25519(k, secret)), + PublicKey::SECP256K1(k) => SecretKey::SECP256K1(map_secp256k1(k, secret)), + } + } } } -// If it's a NEAR-implicit account, interprets it as an ed25519 public key, -// maps that and then returns the resulting implicit account. Otherwise does nothing. -// We do this so that transactions creating an implicit account -// by sending money will generate an account that we can control. +fn map_secp256k1_from_eth_address( + account_id: &AccountId, + secret: Option<&[u8; crate::secret::SECRET_LEN]>, +) -> SecretKey { + let mut buf = [0; secp256k1::constants::SECRET_KEY_SIZE]; + + match secret { + Some(secret) => { + let hk = Hkdf::::new(None, secret); + hk.expand(account_id.as_bytes(), &mut buf).unwrap(); + } + None => { + // We zero-pad ETH address. + buf[0..21].copy_from_slice(account_id.as_bytes()); + } + }; + + let secret_key = secp256k1_from_slice(&mut buf, account_id); + SecretKey::SECP256K1(secret_key) +} + +// If it's a NEAR-implicit account, interprets it as an ed25519 public key, maps that and then returns the resulting implicit account. +// If it's an ETH-implicit account, derive a new secp256k1 secret key from the address, and use the new public key to derive a new ETH-address. +// For named account does nothing. We do this so that transactions creating an implicit account by sending money will generate an account that we can control. pub fn map_account( account_id: &AccountId, secret: Option<&[u8; crate::secret::SECRET_LEN]>, @@ -104,10 +147,13 @@ pub fn map_account( AccountType::NearImplicitAccount => { let public_key = PublicKey::from_near_implicit_account(account_id).expect("must be near-implicit"); - let mapped_key = map_key(&public_key, secret); - derive_near_implicit_account_id(mapped_key.public_key().unwrap_as_ed25519()) + let mapped_key = map_key(&public_key, secret, account_id); + derive_account_id_from_public_key(&mapped_key.public_key()) } - AccountType::EthImplicitAccount => account_id.clone(), + AccountType::EthImplicitAccount => { + let mapped_key = map_secp256k1_from_eth_address(account_id, secret); + derive_account_id_from_public_key(&mapped_key.public_key()) + }, AccountType::NamedAccount => account_id.clone(), } } diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index c09a4b75b33..84c9d210b27 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -971,7 +971,7 @@ impl TxMirror { full_key_added = true; } let public_key = - crate::key_mapping::map_key(&add_key.public_key, self.secret.as_ref()) + crate::key_mapping::map_key(&add_key.public_key, self.secret.as_ref(), tx.receiver_id()) .public_key(); let receiver_id = crate::key_mapping::map_account(tx.receiver_id(), self.secret.as_ref()); @@ -984,7 +984,7 @@ impl TxMirror { } Action::DeleteKey(delete_key) => { let replacement = - crate::key_mapping::map_key(&delete_key.public_key, self.secret.as_ref()); + crate::key_mapping::map_key(&delete_key.public_key, self.secret.as_ref(), tx.receiver_id()); let public_key = replacement.public_key(); actions.push(Action::DeleteKey(Box::new(DeleteKeyAction { public_key }))); @@ -1141,7 +1141,7 @@ impl TxMirror { let mut key = None; let mut first_key = None; for k in keys.iter() { - let target_secret_key = crate::key_mapping::map_key(k, self.secret.as_ref()); + let target_secret_key = crate::key_mapping::map_key(k, self.secret.as_ref(), &receiver_id); if fetch_access_key_nonce( &self.target_view_client, &target_signer_id, @@ -1200,7 +1200,7 @@ impl TxMirror { full_key_added = true; } let target_public_key = - crate::key_mapping::map_key(&a.public_key, self.secret.as_ref()) + crate::key_mapping::map_key(&a.public_key, self.secret.as_ref(), &receiver_id) .public_key(); nonce_updates.insert((target_receiver_id.clone(), target_public_key.clone())); @@ -1521,7 +1521,7 @@ impl TxMirror { continue; } let target_private_key = - crate::key_mapping::map_key(source_tx.public_key(), self.secret.as_ref()); + crate::key_mapping::map_key(source_tx.public_key(), self.secret.as_ref(), source_tx.receiver_id()); let target_signer_id = crate::key_mapping::map_account(source_tx.signer_id(), self.secret.as_ref());