From 62367ed7b3b31a2e96e07fc0f21947710db2b41d Mon Sep 17 00:00:00 2001 From: Bowen Wang Date: Thu, 27 Jul 2023 15:29:57 -0700 Subject: [PATCH] feat(runtime): restrict creation of Ethereum address on NEAR --- core/account-id/src/lib.rs | 40 ++++++++++++++++++ core/primitives-core/Cargo.toml | 2 + core/primitives-core/src/version.rs | 6 ++- core/primitives/Cargo.toml | 24 ++++++++--- integration-tests/Cargo.toml | 3 ++ .../src/tests/standard_cases/mod.rs | 30 +++++++++++++ .../src/tests/standard_cases/runtime.rs | 8 ++++ nearcore/Cargo.toml | 13 +++--- runtime/runtime/Cargo.toml | 8 ++-- runtime/runtime/src/actions.rs | 42 +++++++++++++++++++ runtime/runtime/src/lib.rs | 1 + 11 files changed, 162 insertions(+), 15 deletions(-) diff --git a/core/account-id/src/lib.rs b/core/account-id/src/lib.rs index 196232f03b8..00c32bceab0 100644 --- a/core/account-id/src/lib.rs +++ b/core/account-id/src/lib.rs @@ -104,6 +104,24 @@ impl AccountId { !self.is_system() && !self.contains('.') } + /// Returns `true` if the account id can be viewed as an Ethereum address directly + /// + /// See [Ethereum account](https://ethereum.org/en/developers/docs/accounts/) for more details. + /// + /// ## Examples + /// ``` + /// use near_account_id::AccountId; + /// + /// let eth_addr: AccountId = "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(); + /// assert!(eth_addr.is_ethereum_address()); + /// + /// let non_eth_addr: AccountId = "0x".parse().unwrap(); + /// assert!(!non_eth_addr.is_ethereum_address()); + /// ``` + pub fn is_ethereum_address(&self) -> bool { + self.starts_with("0x") && self.len() == 42 + } + /// Returns `true` if the `AccountId` is a direct sub-account of the provided parent account. /// /// See [Subaccounts](https://docs.near.org/docs/concepts/account#subaccounts). @@ -720,6 +738,28 @@ mod tests { } } + #[test] + fn test_is_ethereum_address() { + let valid_ethereum_addresses = &[ + "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "0x5e97870f263700f46aa00d967821199b9bc5a120", + "0x0000000000000000000000000000000000000000", + ]; + for account_id in valid_ethereum_addresses { + assert!(account_id.parse::().unwrap().is_ethereum_address()); + } + + let non_ethereum_addresses = &[ + "alice.near", + "near", + "0x", + "20782e20662e64666420482123494b6b6c677573646b6c66676a646b6c736667", + ]; + for account_id in non_ethereum_addresses { + assert!(!account_id.parse::().unwrap().is_ethereum_address()); + } + } + #[test] #[cfg(feature = "arbitrary")] fn test_arbitrary() { diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index 2795feab9e1..65e6b7af9c6 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -37,6 +37,7 @@ protocol_feature_fix_contract_loading_cost = [] protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_simple_nightshade_v2 = [] protocol_feature_block_header_v4 = [] +protocol_feature_ethereum_address = [] nightly = [ "nightly_protocol", @@ -45,6 +46,7 @@ nightly = [ "protocol_feature_fix_staking_threshold", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_simple_nightshade_v2", + "protocol_feature_ethereum_address", ] nightly_protocol = [ diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index 5c25776ad4c..d287cf32214 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -124,6 +124,8 @@ pub enum ProtocolFeature { SimpleNightshadeV2, #[cfg(feature = "protocol_feature_block_header_v4")] BlockHeaderV4, + #[cfg(feature = "protocol_feature_ethereum_address")] + EthereumAddress, } impl ProtocolFeature { @@ -178,6 +180,8 @@ impl ProtocolFeature { ProtocolFeature::SimpleNightshadeV2 => 135, #[cfg(feature = "protocol_feature_block_header_v4")] ProtocolFeature::BlockHeaderV4 => 138, + #[cfg(feature = "protocol_feature_ethereum_address")] + ProtocolFeature::EthereumAddress => 139, } } } @@ -190,7 +194,7 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 62; /// Largest protocol version supported by the current binary. pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protocol") { // On nightly, pick big enough version to support all features. - 138 + 139 } else { // Enable all stable features. STABLE_PROTOCOL_VERSION diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 757d537d389..f7d6c82e33a 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -44,13 +44,27 @@ near-vm-runner.workspace = true [features] sandbox = [] dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"] -protocol_feature_fix_staking_threshold = ["near-primitives-core/protocol_feature_fix_staking_threshold"] -protocol_feature_fix_contract_loading_cost = ["near-primitives-core/protocol_feature_fix_contract_loading_cost"] -protocol_feature_reject_blocks_with_outdated_protocol_version = ["near-primitives-core/protocol_feature_reject_blocks_with_outdated_protocol_version"] -protocol_feature_simple_nightshade_v2 = ["near-primitives-core/protocol_feature_simple_nightshade_v2"] -protocol_feature_block_header_v4 = ["near-primitives-core/protocol_feature_block_header_v4"] +protocol_feature_fix_staking_threshold = [ + "near-primitives-core/protocol_feature_fix_staking_threshold", +] +protocol_feature_fix_contract_loading_cost = [ + "near-primitives-core/protocol_feature_fix_contract_loading_cost", +] +protocol_feature_reject_blocks_with_outdated_protocol_version = [ + "near-primitives-core/protocol_feature_reject_blocks_with_outdated_protocol_version", +] +protocol_feature_simple_nightshade_v2 = [ + "near-primitives-core/protocol_feature_simple_nightshade_v2", +] +protocol_feature_block_header_v4 = [ + "near-primitives-core/protocol_feature_block_header_v4", +] +protocol_feature_ethereum_address = [ + "near-primitives-core/protocol_feature_ethereum_address", +] nightly = [ "nightly_protocol", + "protocol_feature_ethereum_address", "protocol_feature_block_header_v4", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 648e3724cd5..aa003e3a780 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -82,6 +82,9 @@ protocol_feature_reject_blocks_with_outdated_protocol_version = [ protocol_feature_simple_nightshade_v2 = [ "near-primitives/protocol_feature_simple_nightshade_v2", ] +protocol_feature_ethereum_address = [ + "nearcore/protocol_feature_ethereum_address", +] nightly = [ "nightly_protocol", diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 65a91e5ee20..7dd513f6ff1 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -665,6 +665,36 @@ pub fn test_create_account_failure_already_exists(node: impl Node) { ); } +pub fn test_create_ethereum_address(node: impl Node) { + let account_id = &node.account_id().unwrap(); + let node_user = node.user(); + let valid_ethereum_addresses = [ + "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "0x5e97870f263700f46aa00d967821199b9bc5a120", + "0x0000000000000000000000000000000000000000", + ]; + for (_, address) in valid_ethereum_addresses.iter().enumerate() { + let new_account_id = address.parse::().unwrap(); + let transaction_result = node_user + .create_account(account_id.clone(), new_account_id.clone(), node.signer().public_key(), 0) + .unwrap(); + assert_eq!( + transaction_result.status, + FinalExecutionStatus::Failure( + ActionError { + index: Some(0), + kind: ActionErrorKind::CreateAccountOnlyByRegistrar { + account_id: new_account_id, + registrar_account_id: "registrar".parse().unwrap(), + predecessor_id: account_id.clone() + } + } + .into() + ) + ); + } +} + pub fn test_swap_key(node: impl Node) { let account_id = &node.account_id().unwrap(); let signer2 = InMemorySigner::from_random("test".parse().unwrap(), KeyType::ED25519); diff --git a/integration-tests/src/tests/standard_cases/runtime.rs b/integration-tests/src/tests/standard_cases/runtime.rs index a5cae097047..7cd067a1081 100644 --- a/integration-tests/src/tests/standard_cases/runtime.rs +++ b/integration-tests/src/tests/standard_cases/runtime.rs @@ -344,3 +344,11 @@ fn test_storage_read_write_costs_runtime() { let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); test_storage_read_write_costs(node, runtime_config); } + +#[test] +#[cfg(feature = "protocol_feature_ethereum_address")] +fn test_create_ethereum_address_runtime() { + let node = create_runtime_node(); + let runtime_config = node.client.as_ref().read().unwrap().runtime_config.clone(); + test_create_ethereum_address(node); +} diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 194a1a5b702..d2867cd768c 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -84,15 +84,13 @@ harness = false [features] default = ["json_rpc", "rosetta_rpc"] -performance_stats = [ - "near-performance-metrics/performance_stats", -] +performance_stats = ["near-performance-metrics/performance_stats"] c_memory_stats = ["near-performance-metrics/c_memory_stats"] test_features = [ "near-client/test_features", "near-network/test_features", "near-store/test_features", - "near-jsonrpc/test_features" + "near-jsonrpc/test_features", ] expensive_tests = [ "near-client/expensive_tests", @@ -116,12 +114,17 @@ protocol_feature_fix_contract_loading_cost = [ "near-vm-runner/protocol_feature_fix_contract_loading_cost", ] protocol_feature_simple_nightshade_v2 = [ - "near-primitives/protocol_feature_simple_nightshade_v2", + "near-primitives/protocol_feature_simple_nightshade_v2", +] +protocol_feature_ethereum_address = [ + "near-primitives/protocol_feature_ethereum_address", + "node-runtime/protocol_feature_ethereum_address", ] serialize_all_state_changes = ["near-store/serialize_all_state_changes"] nightly = [ "nightly_protocol", + "protocol_feature_ethereum_address", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", "protocol_feature_simple_nightshade_v2", diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 9321c338d1a..91392738c7c 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -31,6 +31,7 @@ near-store.workspace = true near-vm-runner.workspace = true [features] +protocol_feature_ethereum_address = ["near-primitives/protocol_feature_ethereum_address"] nightly = [ "nightly_protocol", "near-chain-configs/nightly", @@ -38,6 +39,7 @@ nightly = [ "near-primitives/nightly", "near-store/nightly", "near-vm-runner/nightly", + "protocol_feature_ethereum_address", ] default = [] nightly_protocol = [ @@ -46,13 +48,11 @@ nightly_protocol = [ "near-primitives/nightly_protocol", "near-store/nightly_protocol", "near-vm-runner/nightly_protocol", + "protocol_feature_ethereum_address", ] no_cpu_compatibility_checks = ["near-vm-runner/no_cpu_compatibility_checks"] -no_cache = [ - "near-vm-runner/no_cache", - "near-store/no_cache", -] +no_cache = ["near-vm-runner/no_cache", "near-store/no_cache"] sandbox = ["near-vm-runner/sandbox"] diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 82386289880..969fda045cc 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -382,6 +382,7 @@ pub(crate) fn action_create_account( account_id: &AccountId, predecessor_id: &AccountId, result: &mut ActionResult, + current_protocol_version: ProtocolVersion, ) { if account_id.is_top_level() { if account_id.len() < account_creation_config.min_allowed_top_level_account_length as usize @@ -395,6 +396,23 @@ pub(crate) fn action_create_account( } .into()); return; + } else if checked_feature!( + "protocol_feature_ethereum_address", + EthereumAddress, + current_protocol_version + ) { + if account_id.is_ethereum_address() + && predecessor_id != &account_creation_config.registrar_account_id + { + // An Ethereum address can only be created by the registrar account + result.result = Err(ActionErrorKind::CreateAccountOnlyByRegistrar { + account_id: account_id.clone(), + registrar_account_id: account_creation_config.registrar_account_id.clone(), + predecessor_id: predecessor_id.clone(), + } + .into()); + return; + } } else { // OK: Valid top-level Account ID } @@ -967,6 +985,7 @@ mod tests { use near_primitives::transaction::CreateAccountAction; use near_primitives::trie_key::TrieKey; use near_primitives::types::{EpochId, StateChangeCause}; + use near_primitives::version::PROTOCOL_VERSION; use near_store::set_account; use near_store::test_utils::create_tries; use std::sync::Arc; @@ -990,6 +1009,7 @@ mod tests { &account_id, &predecessor_id, &mut action_result, + PROTOCOL_VERSION, ); if action_result.result.is_ok() { assert!(account.is_some()); @@ -1069,6 +1089,28 @@ mod tests { assert!(action_result.result.is_ok()); } + #[test] + #[cfg(feature = "protocol_feature_ethereum_address")] + fn test_create_ethereum_address() { + let account_id: AccountId = "0x32400084c286cf3e17e7b677ea9583e60a000324".parse().unwrap(); + let predecessor_id: AccountId = "near".parse().unwrap(); + let action_result = test_action_create_account(account_id.clone(), predecessor_id.clone(), 32); + assert_eq!( + action_result.result, + Err(ActionError { + index: None, + kind: ActionErrorKind::CreateAccountOnlyByRegistrar { + account_id: account_id.clone(), + registrar_account_id: "registrar".parse().unwrap(), + predecessor_id: predecessor_id, + }, + }) + ); + let registrar_account_id: AccountId = "registrar".parse().unwrap(); + let action_result = test_action_create_account(account_id, registrar_account_id, 32); + assert!(action_result.result.is_ok()); + } + fn test_delete_large_account( account_id: &AccountId, code_hash: &CryptoHash, diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 34c28f3de9a..fbd15f6059e 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -339,6 +339,7 @@ impl Runtime { &receipt.receiver_id, &receipt.predecessor_id, &mut result, + apply_state.current_protocol_version, ); } Action::DeployContract(deploy_contract) => {