From f363848dd7ae3262578e248ef55b668e37eb37db Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 25 Jul 2023 20:14:43 +0200 Subject: [PATCH 01/36] NEP-491 reference implementation The purpose of this reference implementation is to show the proposed protocol changes in practice. It should be sufficient to convince the reader that all technical challenges have been identified and that there is a solution. However, this is not a full implementation. - tests are not fixed - boilerplate code changes are left as todo!() - (rosetta) rpc node code does not compile - changes are not behind feature flags - some refactoring would make sense --- chain/chain/src/test_utils/kv_runtime.rs | 1 + chain/rosetta-rpc/src/adapters/mod.rs | 2 + chain/rosetta-rpc/src/lib.rs | 2 +- core/primitives-core/src/account.rs | 108 +++++++++++++++++--- core/primitives/src/test_utils.rs | 2 +- core/primitives/src/views.rs | 6 +- genesis-tools/genesis-populate/src/lib.rs | 1 + nearcore/src/config.rs | 2 +- runtime/near-vm-runner/src/logic/action.rs | 21 ++++ runtime/runtime/src/actions.rs | 50 +++++++-- runtime/runtime/src/config.rs | 4 +- runtime/runtime/src/lib.rs | 39 ++++++- runtime/runtime/src/verifier.rs | 7 +- test-utils/testlib/src/runtime_utils.rs | 4 +- tools/amend-genesis/src/lib.rs | 30 ++++-- tools/fork-network/src/cli.rs | 8 +- tools/state-viewer/src/contract_accounts.rs | 1 + 17 files changed, 249 insertions(+), 39 deletions(-) diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 9105e21ba47..67522e54a16 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -1225,6 +1225,7 @@ impl RuntimeAdapter for KeyValueRuntime { |state| *state.amounts.get(account_id).unwrap_or(&0), ), 0, + 0, CryptoHash::default(), 0, ) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 4f3ea2500ac..741f4a34efa 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -345,6 +345,8 @@ impl From for Vec { ); } + near_primitives::transaction::Action::TransferV2(_) => todo!("TODO(jakmeier)"), + near_primitives::transaction::Action::Stake(action) => { operations.push( validated_operations::StakeOperation { diff --git a/chain/rosetta-rpc/src/lib.rs b/chain/rosetta-rpc/src/lib.rs index a018e5ea91e..3a1228aed56 100644 --- a/chain/rosetta-rpc/src/lib.rs +++ b/chain/rosetta-rpc/src/lib.rs @@ -354,7 +354,7 @@ async fn account_balance( Err(crate::errors::ErrorKind::NotFound(_)) => ( block.header.hash, block.header.height, - near_primitives::account::Account::new(0, 0, Default::default(), 0).into(), + near_primitives::account::Account::new(0, 0, 0, Default::default(), 0).into(), ), Err(err) => return Err(err.into()), }; diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 50a3a340d52..9ce3fb7347f 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -18,24 +18,34 @@ use std::io; serde::Deserialize, )] pub enum AccountVersion { - #[default] V1, + // TODO(jakmeier): hide behind feature flag + #[default] + V2, } /// Per account information stored in the state. #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Account { - /// The total not locked tokens. + /// The total not locked, refundable tokens. #[serde(with = "dec_format")] amount: Balance, /// The amount locked due to staking. #[serde(with = "dec_format")] locked: Balance, + // TODO(jakmeier): hide behind feature flag, we don't want to show this field in serde::Deserialize unless the feature is active + /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. + #[serde(with = "dec_format")] + nonrefundable: Balance, /// Hash of the code stored in the storage for this account. code_hash: CryptoHash, /// Storage used by the given account, includes account id, this struct, access keys and other data. storage_usage: StorageUsage, /// Version of Account in re migrations and similar + /// + /// Note(jakmeier): Why does this exist? We only have one version right now + /// and the code doesn't allow adding a new version at all since this field + /// is not included in the merklized state... #[serde(default)] version: AccountVersion, } @@ -44,14 +54,27 @@ impl Account { /// Max number of bytes an account can have in its state (excluding contract code) /// before it is infeasible to delete. pub const MAX_ACCOUNT_DELETION_STORAGE_USAGE: u64 = 10_000; + /// HACK: Using u128::MAX as a sentinel value, there are not enough tokens + /// in total supply which makes it an invalid value. We use it to + /// differentiate AccountVersion V1 from newer versions. + const SERIALIZATION_SENTINEL: u128 = u128::MAX; pub fn new( amount: Balance, locked: Balance, + nonrefundable: Balance, code_hash: CryptoHash, storage_usage: StorageUsage, ) -> Self { - Account { amount, locked, code_hash, storage_usage, version: AccountVersion::V1 } + Account { + amount, + locked, + nonrefundable, + code_hash, + storage_usage, + // TODO(jakmeier): condition on feature flag + version: AccountVersion::V2, + } } #[inline] @@ -59,6 +82,11 @@ impl Account { self.amount } + #[inline] + pub fn nonrefundable(&self) -> Balance { + self.nonrefundable + } + #[inline] pub fn locked(&self) -> Balance { self.locked @@ -84,6 +112,11 @@ impl Account { self.amount = amount; } + #[inline] + pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { + self.nonrefundable = nonrefundable; + } + #[inline] pub fn set_locked(&mut self, locked: Balance) { self.locked = locked; @@ -104,7 +137,9 @@ impl Account { } } -#[derive(BorshSerialize, BorshDeserialize)] +/// Note(jakmeier): Even though this is called "legacy", it looks like this is +/// the one and only serialization format of Accounts currently in use. +#[derive(BorshSerialize)] struct LegacyAccount { amount: Balance, locked: Balance, @@ -114,16 +149,36 @@ struct LegacyAccount { impl BorshDeserialize for Account { fn deserialize_reader(rd: &mut R) -> io::Result { - // This should only ever happen if we have pre-transition account serialized in state - // See test_account_size - let deserialized_account = LegacyAccount::deserialize_reader(rd)?; - Ok(Account { - amount: deserialized_account.amount, - locked: deserialized_account.locked, - code_hash: deserialized_account.code_hash, - storage_usage: deserialized_account.storage_usage, - version: AccountVersion::V1, - }) + // The first value of all Account serialization formats is a u128, + // either a sentinel or a balance. + let sentinel_or_amount = u128::deserialize_reader(rd)?; + if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { + // Account v2 or newer + let version_byte = u8::deserialize_reader(rd)?; + assert_eq!(version_byte, 2); // TODO(jakmeier): return proper error instead of panic + let version = AccountVersion::V2; + let amount = u128::deserialize_reader(rd)?; + let locked = u128::deserialize_reader(rd)?; + let code_hash = CryptoHash::deserialize_reader(rd)?; + let storage_usage = StorageUsage::deserialize_reader(rd)?; + let nonrefundable = u128::deserialize_reader(rd)?; + + Ok(Account { amount, locked, code_hash, storage_usage, version, nonrefundable }) + } else { + // Account v1 + let locked = u128::deserialize_reader(rd)?; + let code_hash = CryptoHash::deserialize_reader(rd)?; + let storage_usage = StorageUsage::deserialize_reader(rd)?; + + Ok(Account { + amount: sentinel_or_amount, + locked, + code_hash, + storage_usage, + version: AccountVersion::V1, + nonrefundable: 0, + }) + } } } @@ -135,8 +190,33 @@ impl BorshSerialize for Account { locked: self.locked, code_hash: self.code_hash, storage_usage: self.storage_usage, + // FIXME(jakmeier): can we add nonrefundable storage to existing + // accounts? IN this implementation, we burn any nonrefundable + // tokens sent to existing accounts, which is unacceptable. But + // automatically converting old V1 to V2 would break the borsh + // assumptions of unique binary representation. } .serialize(writer), + // TODO(jakmeier): Can we do better than this? + // Context: These accounts are serialized in merklized state. I + // would really like to avoid migration of the MPT. This here would + // keep old accounts in the old format and only allow nonrefundable + // storage on new accounts. + AccountVersion::V2 => { + let sentinel = Account::SERIALIZATION_SENTINEL; + // For now a constant, but if we need V3 later we can use this + // field instead of sentinel magic. + let version = 2u8; + BorshSerialize::serialize(&sentinel, writer)?; + BorshSerialize::serialize(&version, writer)?; + // TODO(jakmeier): Consider wrapping this in a struct and derive BorshSerialize for it. + BorshSerialize::serialize(&self.amount, writer)?; + BorshSerialize::serialize(&self.locked, writer)?; + BorshSerialize::serialize(&self.code_hash, writer)?; + BorshSerialize::serialize(&self.storage_usage, writer)?; + BorshSerialize::serialize(&self.nonrefundable, writer)?; + Ok(()) + } } } } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 4abfe6879b7..53c6358afb1 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -27,7 +27,7 @@ use crate::version::PROTOCOL_VERSION; use crate::views::{ExecutionStatusView, FinalExecutionOutcomeView, FinalExecutionStatus}; pub fn account_new(amount: Balance, code_hash: CryptoHash) -> Account { - Account::new(amount, 0, code_hash, std::mem::size_of::() as u64) + Account::new(amount, 0, 0, code_hash, std::mem::size_of::() as u64) } impl Transaction { diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 0681d198445..03a291e1191 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -122,7 +122,8 @@ impl From for AccountView { impl From<&AccountView> for Account { fn from(view: &AccountView) -> Self { - Account::new(view.amount, view.locked, view.code_hash, view.storage_usage) + // TODO(jakmeier): expose nonrefundable storage on account view + Account::new(view.amount, view.locked, 0, view.code_hash, view.storage_usage) } } @@ -1212,6 +1213,9 @@ impl From for ActionView { deposit: action.deposit, }, Action::Transfer(action) => ActionView::Transfer { deposit: action.deposit }, + Action::TransferV2(..) => { + todo!("TODO(jakmeier") + } Action::Stake(action) => { ActionView::Stake { stake: action.stake, public_key: action.public_key } } diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index cfe071a5046..37bbd4272f1 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -279,6 +279,7 @@ impl GenesisBuilder { let account = Account::new( testing_init_balance, testing_init_stake, + 0, self.additional_accounts_code_hash, 0, ); diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index ae677c815e7..12d5852b207 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -773,7 +773,7 @@ fn add_account_with_key( ) { records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(amount, staked, code_hash, 0), + account: Account::new(amount, staked, 0, code_hash, 0), }); records.push(StateRecord::AccessKey { account_id, diff --git a/runtime/near-vm-runner/src/logic/action.rs b/runtime/near-vm-runner/src/logic/action.rs index 6e04c956dee..7717dfaeb4b 100644 --- a/runtime/near-vm-runner/src/logic/action.rs +++ b/runtime/near-vm-runner/src/logic/action.rs @@ -150,6 +150,24 @@ pub struct TransferAction { pub deposit: Balance, } +#[derive( + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, + Clone, + Debug, + serde::Serialize, + serde::Deserialize, +)] +// TODO(jakmeier): hide behind feature flag +pub struct TransferActionV2 { + #[serde(with = "dec_format")] + pub deposit: Balance, + /// If this flag is set, the balance will be added to the receiver's non-refundable balance. + pub nonrefundable: bool, +} + #[derive( BorshSerialize, BorshDeserialize, @@ -169,12 +187,15 @@ pub enum Action { /// Sets a Wasm code to a receiver_id DeployContract(DeployContractAction), FunctionCall(FunctionCallAction), + /// To be deprecated with NEP XXX Transfer(TransferAction), Stake(StakeAction), AddKey(AddKeyAction), DeleteKey(DeleteKeyAction), DeleteAccount(DeleteAccountAction), Delegate(super::delegate_action::SignedDelegateAction), + /// TODO: hide behind cfg feature + TransferV2(TransferActionV2), } impl Action { diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 82386289880..549107e2881 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -21,7 +21,9 @@ use near_primitives::transaction::{ FunctionCallAction, StakeAction, TransferAction, }; use near_primitives::types::validator_stake::ValidatorStake; -use near_primitives::types::{AccountId, BlockHeight, EpochInfoProvider, Gas, TrieCacheMode}; +use near_primitives::types::{ + AccountId, Balance, BlockHeight, EpochInfoProvider, Gas, TrieCacheMode, +}; use near_primitives::utils::create_random_seed; use near_primitives::version::{ ProtocolFeature, ProtocolVersion, DELETE_KEY_STORAGE_USAGE_PROTOCOL_VERSION, @@ -30,6 +32,7 @@ use near_store::{ get_access_key, get_code, remove_access_key, remove_account, set_access_key, set_code, StorageError, TrieUpdate, }; +use near_vm_runner::logic::action::TransferActionV2; use near_vm_runner::logic::errors::{ CompilationError, FunctionCallError, InconsistentStateError, VMRunnerError, }; @@ -342,7 +345,7 @@ pub(crate) fn try_refund_allowance( state_update: &mut TrieUpdate, account_id: &AccountId, public_key: &PublicKey, - transfer: &TransferAction, + deposit: Balance, ) -> Result<(), StorageError> { if let Some(mut access_key) = get_access_key(state_update, account_id, public_key)? { let mut updated = false; @@ -350,7 +353,7 @@ pub(crate) fn try_refund_allowance( &mut access_key.permission { if let Some(allowance) = function_call_permission.allowance.as_mut() { - let new_allowance = allowance.saturating_add(transfer.deposit); + let new_allowance = allowance.saturating_add(deposit); if new_allowance > *allowance { *allowance = new_allowance; updated = true; @@ -374,6 +377,26 @@ pub(crate) fn action_transfer( Ok(()) } +pub(crate) fn action_transfer_v2( + account: &mut Account, + transfer: &TransferActionV2, +) -> Result<(), StorageError> { + if transfer.nonrefundable { + account.set_nonrefundable( + account.nonrefundable().checked_add(transfer.deposit).ok_or_else(|| { + StorageError::StorageInconsistentState( + "Non-refundable account balance integer overflow".to_string(), + ) + })?, + ); + } else { + account.set_amount(account.amount().checked_add(transfer.deposit).ok_or_else(|| { + StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) + })?); + } + Ok(()) +} + pub(crate) fn action_create_account( fee_config: &RuntimeFeesConfig, account_creation_config: &AccountCreationConfig, @@ -412,6 +435,7 @@ pub(crate) fn action_create_account( *actor_id = account_id.clone(); *account = Some(Account::new( + 0, 0, 0, CryptoHash::default(), @@ -425,9 +449,10 @@ pub(crate) fn action_implicit_account_creation_transfer( account: &mut Option, actor_id: &mut AccountId, account_id: &AccountId, - transfer: &TransferAction, + deposit: Balance, block_height: BlockHeight, current_protocol_version: ProtocolVersion, + nonrefundable: bool, ) { *actor_id = account_id.clone(); @@ -444,9 +469,21 @@ pub(crate) fn action_implicit_account_creation_transfer( // unwrap: Can only fail if `account_id` is not implicit. let public_key = PublicKey::from_implicit_account(account_id).unwrap(); + // TODO(jakmeier): feature flag? + let refundable_balance; + let nonrefundable_balance; + if nonrefundable { + refundable_balance = 0; + nonrefundable_balance = deposit; + } else { + refundable_balance = deposit; + nonrefundable_balance = 0; + } + *account = Some(Account::new( - transfer.deposit, + refundable_balance, 0, + nonrefundable_balance, CryptoHash::default(), fee_config.storage_usage_config.num_bytes_account + public_key.len() as u64 @@ -865,6 +902,7 @@ pub(crate) fn check_actor_permissions( } Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), Action::Delegate(_) => (), + Action::TransferV2(_) => todo!("TODO(jakmeier"), }; Ok(()) } @@ -904,7 +942,7 @@ pub(crate) fn check_account_existence( } } } - Action::Transfer(_) => { + Action::Transfer(_) | Action::TransferV2(_) => { if account.is_none() { return if checked_feature!( "stable", diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 095274d4146..1784107ab5f 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -99,7 +99,7 @@ pub fn total_send_fees( + config.fee(ActionCosts::function_call_byte).send_fee(sender_is_receiver) * num_bytes } - Transfer(_) => { + Transfer(_) | TransferV2(_) => { // Account for implicit account creation let is_receiver_implicit = checked_feature!("stable", ImplicitAccountCreation, current_protocol_version) @@ -202,7 +202,7 @@ pub fn exec_fee( config.fee(ActionCosts::function_call_base).exec_fee() + config.fee(ActionCosts::function_call_byte).exec_fee() * num_bytes } - Transfer(_) => { + Transfer(_) | TransferV2(_) => { // Account for implicit account creation let is_receiver_implicit = checked_feature!("stable", ImplicitAccountCreation, current_protocol_version) diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 34c28f3de9a..9069531c66e 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -377,7 +377,7 @@ impl Runtime { state_update, &receipt.receiver_id, &action_receipt.signer_public_key, - transfer, + transfer.deposit, )?; } } else { @@ -394,9 +394,44 @@ impl Runtime { account, actor_id, &receipt.receiver_id, - transfer, + transfer.deposit, apply_state.block_height, apply_state.current_protocol_version, + false, + ); + } + } + Action::TransferV2(transfer) => { + // TOOD(jakmeier): This is just a copy-paste of `Transfer` with two lines changed, it should be refactored + if let Some(account) = account.as_mut() { + action_transfer_v2(account, transfer)?; + // Check if this is a gas refund, then try to refund the access key allowance. + if is_refund && action_receipt.signer_id == receipt.receiver_id { + try_refund_allowance( + state_update, + &receipt.receiver_id, + &action_receipt.signer_public_key, + transfer.deposit, + )?; + } + } else { + // Implicit account creation + debug_assert!(checked_feature!( + "stable", + ImplicitAccountCreation, + apply_state.current_protocol_version + )); + debug_assert!(!is_refund); + action_implicit_account_creation_transfer( + state_update, + &apply_state.config.fees, + account, + actor_id, + &receipt.receiver_id, + transfer.deposit, + apply_state.block_height, + apply_state.current_protocol_version, + transfer.nonrefundable, ); } } diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 8cbc784a9f7..1e50327a597 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -58,11 +58,13 @@ pub fn check_storage_stake( let available_amount = account .amount() .checked_add(account.locked()) + .and_then(|amount| amount.checked_add(account.nonrefundable())) .ok_or_else(|| { format!( - "Account's amount {} and locked {} overflow addition", + "Account's amount {}, locked {}, and non-refundable {} overflow addition", account.amount(), - account.locked() + account.locked(), + account.nonrefundable(), ) }) .map_err(StorageStakingError::StorageError)?; @@ -401,6 +403,7 @@ pub fn validate_action( Action::DeployContract(a) => validate_deploy_contract_action(limit_config, a), Action::FunctionCall(a) => validate_function_call_action(limit_config, a), Action::Transfer(_) => Ok(()), + Action::TransferV2(_) => todo!("TODO(jakmeier"), Action::Stake(a) => validate_stake_action(a), Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), diff --git a/test-utils/testlib/src/runtime_utils.rs b/test-utils/testlib/src/runtime_utils.rs index eafbf8396be..8c9c0e13340 100644 --- a/test-utils/testlib/src/runtime_utils.rs +++ b/test-utils/testlib/src/runtime_utils.rs @@ -46,7 +46,7 @@ pub fn add_contract(genesis: &mut Genesis, account_id: &AccountId, code: Vec if !is_account_record_found { records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(0, 0, hash, 0), + account: Account::new(0, 0, 0, hash, 0), }); } records.push(StateRecord::Contract { account_id: account_id.clone(), code }); @@ -63,7 +63,7 @@ pub fn add_account_with_access_key( let records = genesis.force_read_records().as_mut(); records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(balance, 0, Default::default(), 0), + account: Account::new(balance, 0, 0, Default::default(), 0), }); records.push(StateRecord::AccessKey { account_id, public_key, access_key }); } diff --git a/tools/amend-genesis/src/lib.rs b/tools/amend-genesis/src/lib.rs index 391745690b1..07d31ce4af4 100644 --- a/tools/amend-genesis/src/lib.rs +++ b/tools/amend-genesis/src/lib.rs @@ -47,22 +47,34 @@ fn set_total_balance(dst: &mut Account, src: &Account) { } impl AccountRecords { - fn new(amount: Balance, locked: Balance, num_bytes_account: u64) -> Self { + fn new( + amount: Balance, + locked: Balance, + nonrefundable: Balance, + num_bytes_account: u64, + ) -> Self { let mut ret = Self::default(); - ret.set_account(amount, locked, num_bytes_account); + ret.set_account(amount, locked, nonrefundable, num_bytes_account); ret } fn new_validator(stake: Balance, num_bytes_account: u64) -> Self { let mut ret = Self::default(); - ret.set_account(0, stake, num_bytes_account); + ret.set_account(0, stake, 0, num_bytes_account); ret.amount_needed = true; ret } - fn set_account(&mut self, amount: Balance, locked: Balance, num_bytes_account: u64) { + fn set_account( + &mut self, + amount: Balance, + locked: Balance, + nonrefundable: Balance, + num_bytes_account: u64, + ) { assert!(self.account.is_none()); - let account = Account::new(amount, locked, CryptoHash::default(), num_bytes_account); + let account = + Account::new(amount, locked, nonrefundable, CryptoHash::default(), num_bytes_account); self.account = Some(account); } @@ -181,6 +193,7 @@ fn parse_extra_records( let r = AccountRecords::new( account.amount(), account.locked(), + account.nonrefundable(), num_bytes_account, ); e.insert(r); @@ -194,7 +207,12 @@ fn parse_extra_records( &account_id )); } - r.set_account(account.amount(), account.locked(), num_bytes_account); + r.set_account( + account.amount(), + account.locked(), + account.nonrefundable(), + num_bytes_account, + ); } } } diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index 3bf6dcbeef5..c279aad211a 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -163,7 +163,13 @@ impl ForkNetworkCommand { let liquid_balance = 100_000_000 * NEAR_BASE; storage_mutator.set_account( self_validator.validator_id().clone(), - Account::new(liquid_balance, self_account.amount, CryptoHash::default(), storage_bytes), + Account::new( + liquid_balance, + self_account.amount, + 0, + CryptoHash::default(), + storage_bytes, + ), )?; storage_mutator.set_access_key( self_account.account_id.clone(), diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index 2fb38582b23..96c940dbb02 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -334,6 +334,7 @@ fn try_find_actions_spawned_by_receipt( Action::DeployContract(_) => ActionType::DeployContract, Action::FunctionCall(_) => ActionType::FunctionCall, Action::Transfer(_) => ActionType::Transfer, + Action::TransferV2(_) => ActionType::Transfer, Action::Stake(_) => ActionType::Stake, Action::AddKey(_) => ActionType::AddKey, Action::DeleteKey(_) => ActionType::DeleteKey, From f1019962c3c1c8d49d03f5e5dd247f4bf3f43a6c Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 26 Jul 2023 12:43:52 +0200 Subject: [PATCH 02/36] reject non-refundable transfer to existing account introduces the error variant `NonRefundableBalanceToExistingAccount` --- core/primitives/src/errors.rs | 5 ++++ runtime/runtime/src/actions.rs | 43 ++++++++++++++++++++++++++++++++- runtime/runtime/src/lib.rs | 4 +++ runtime/runtime/src/verifier.rs | 1 + 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 9ede1561aba..5475fa86623 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -493,6 +493,8 @@ pub enum ActionErrorKind { DelegateActionInvalidNonce { delegate_nonce: Nonce, ak_nonce: Nonce }, /// DelegateAction nonce is larger than the upper bound given by the block height DelegateActionNonceTooLarge { delegate_nonce: Nonce, upper_bound: Nonce }, + /// Sending non-refundable balance to an existing account is not allowed according to NEP-491. + NonRefundableBalanceToExistingAccount { account_id: AccountId }, } impl From for ActionError { @@ -817,6 +819,9 @@ impl Display for ActionErrorKind { ActionErrorKind::DelegateActionAccessKeyError(access_key_error) => Display::fmt(&access_key_error, f), ActionErrorKind::DelegateActionInvalidNonce { delegate_nonce, ak_nonce } => write!(f, "DelegateAction nonce {} must be larger than nonce of the used access key {}", delegate_nonce, ak_nonce), ActionErrorKind::DelegateActionNonceTooLarge { delegate_nonce, upper_bound } => write!(f, "DelegateAction nonce {} must be smaller than the access key nonce upper bound {}", delegate_nonce, upper_bound), + ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id} => { + write!(f, "Can't send non-refundable balance to {} because it already exists", account_id) + } } } } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 549107e2881..6c3edba36b4 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -914,6 +914,7 @@ pub(crate) fn check_account_existence( current_protocol_version: ProtocolVersion, is_the_only_action: bool, is_refund: bool, + receipt_starts_with_create_account: bool, ) -> Result<(), ActionError> { match action { Action::CreateAccount(_) => { @@ -942,7 +943,7 @@ pub(crate) fn check_account_existence( } } } - Action::Transfer(_) | Action::TransferV2(_) => { + Action::Transfer(_) => { if account.is_none() { return if checked_feature!( "stable", @@ -967,6 +968,46 @@ pub(crate) fn check_account_existence( }; } } + Action::TransferV2(transfer) => { + // TODO(jakmeier): consider refactoring the code duplication + if account.is_none() { + return if checked_feature!( + "stable", + ImplicitAccountCreation, + current_protocol_version + ) && is_the_only_action + && account_id.is_implicit() + && !is_refund + { + // OK. It's implicit account creation. + // Notes: + // - The transfer action has to be the only action in the transaction to avoid + // abuse by hijacking this account with other public keys or contracts. + // - Refunds don't automatically create accounts, because refunds are free and + // we don't want some type of abuse. + // - Account deletion with beneficiary creates a refund, so it'll not create a + // new account. + Ok(()) + } else { + Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone() } + .into()) + }; + } else if transfer.nonrefundable && !receipt_starts_with_create_account { + // If the account already existed before the current receipt, + // non-refundable transfer is not allowed. But for named + // accounts, it could be that the account was created in this + // receipt which is allowed. Checking for the first action of + // the receipt being a `CreateAccount` action serves this + // purpose. + // For implicit accounts, it is impossible that the account was + // created by a prior action in the receipt because they must be + // created with a singleton receipt. + return Err(ActionErrorKind::NonRefundableBalanceToExistingAccount { + account_id: account_id.clone(), + } + .into()); + } + } Action::DeployContract(_) | Action::FunctionCall(_) | Action::Stake(_) diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 9069531c66e..c7a6d716d8d 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -311,6 +311,9 @@ impl Runtime { let account_id = &receipt.receiver_id; let is_the_only_action = actions.len() == 1; let is_refund = AccountId::is_system(&receipt.predecessor_id); + + let receipt_starts_with_create_account = + matches!(actions.get(0), Some(Action::CreateAccount(_))); // Account validation if let Err(e) = check_account_existence( action, @@ -319,6 +322,7 @@ impl Runtime { apply_state.current_protocol_version, is_the_only_action, is_refund, + receipt_starts_with_create_account, ) { result.result = Err(e); return Ok(result); diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 1e50327a597..5b6166cd5d5 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -359,6 +359,7 @@ pub(crate) fn validate_actions( let mut found_delegate_action = false; let mut iter = actions.iter().peekable(); while let Some(action) = iter.next() { + // TODO(jakmeier): make sure TransferV2 gets rejected in older protocol versions if let Action::DeleteAccount(_) = action { if iter.peek().is_some() { return Err(ActionsValidationError::DeleteActionMustBeFinal); From ab967fe8f33da474a5cd5595e258de806f4007dc Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 26 Sep 2023 14:22:46 +0200 Subject: [PATCH 03/36] handle transferV2 in rosetta RPC --- chain/rosetta-rpc/src/adapters/mod.rs | 14 +++++++++----- core/primitives/src/transaction.rs | 2 +- runtime/near-vm-runner/src/logic/action.rs | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 741f4a34efa..8a6160b0eb6 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -2,6 +2,7 @@ use actix::Addr; use near_chain_configs::Genesis; use near_client::ViewClientActor; use near_o11y::WithSpanContextExt; +use near_primitives::transaction::{TransferAction, TransferActionV2}; use validated_operations::ValidatedOperation; mod transactions; @@ -318,8 +319,14 @@ impl From for Vec { ); } - near_primitives::transaction::Action::Transfer(action) => { - let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); + // Note: Both refundable and non-refundable transfers are considered as available balance. + // (TODO: ensure final decision for NEP-491 aligns with that!) + near_primitives::transaction::Action::Transfer(TransferAction { deposit }) + | near_primitives::transaction::Action::TransferV2(TransferActionV2 { + deposit, + .. + }) => { + let transfer_amount = crate::models::Amount::from_yoctonear(deposit); let sender_transfer_operation_id = crate::models::OperationIdentifier::new(&operations); @@ -344,9 +351,6 @@ impl From for Vec { ), ); } - - near_primitives::transaction::Action::TransferV2(_) => todo!("TODO(jakmeier)"), - near_primitives::transaction::Action::Stake(action) => { operations.push( validated_operations::StakeOperation { diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 007bfb2ca94..b87b71f2adc 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -13,7 +13,7 @@ use std::hash::{Hash, Hasher}; pub use near_vm_runner::logic::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, - DeployContractAction, FunctionCallAction, StakeAction, TransferAction, + DeployContractAction, FunctionCallAction, StakeAction, TransferAction, TransferActionV2, }; pub type LogEntry = String; diff --git a/runtime/near-vm-runner/src/logic/action.rs b/runtime/near-vm-runner/src/logic/action.rs index 7717dfaeb4b..9af7cbe70cf 100644 --- a/runtime/near-vm-runner/src/logic/action.rs +++ b/runtime/near-vm-runner/src/logic/action.rs @@ -187,7 +187,7 @@ pub enum Action { /// Sets a Wasm code to a receiver_id DeployContract(DeployContractAction), FunctionCall(FunctionCallAction), - /// To be deprecated with NEP XXX + /// To be deprecated with NEP-491 but kept for backwards-compatibility. Transfer(TransferAction), Stake(StakeAction), AddKey(AddKeyAction), From 1b01356fc711c05c8673bedd34d90df40206bd2c Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 26 Sep 2023 15:24:25 +0200 Subject: [PATCH 04/36] refactor duplicate implicit account creation check --- runtime/runtime/src/actions.rs | 74 +++++++++++++++++----------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 5282e308ff4..71472d8e0aa 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -942,47 +942,22 @@ pub(crate) fn check_account_existence( } Action::Transfer(_) => { if account.is_none() { - return if config.wasm_config.implicit_account_creation - && is_the_only_action - && account_id.is_implicit() - && !is_refund - { - // OK. It's implicit account creation. - // Notes: - // - The transfer action has to be the only action in the transaction to avoid - // abuse by hijacking this account with other public keys or contracts. - // - Refunds don't automatically create accounts, because refunds are free and - // we don't want some type of abuse. - // - Account deletion with beneficiary creates a refund, so it'll not create a - // new account. - Ok(()) - } else { - Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone() } - .into()) - }; + return check_transfer_to_nonexisting_account( + config, + is_the_only_action, + account_id, + is_refund, + ); } } Action::TransferV2(transfer) => { - // TODO(jakmeier): consider refactoring the code duplication if account.is_none() { - return if config.wasm_config.implicit_account_creation - && is_the_only_action - && account_id.is_implicit() - && !is_refund - { - // OK. It's implicit account creation. - // Notes: - // - The transfer action has to be the only action in the transaction to avoid - // abuse by hijacking this account with other public keys or contracts. - // - Refunds don't automatically create accounts, because refunds are free and - // we don't want some type of abuse. - // - Account deletion with beneficiary creates a refund, so it'll not create a - // new account. - Ok(()) - } else { - Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone() } - .into()) - }; + return check_transfer_to_nonexisting_account( + config, + is_the_only_action, + account_id, + is_refund, + ); } else if transfer.nonrefundable && !receipt_starts_with_create_account { // If the account already existed before the current receipt, // non-refundable transfer is not allowed. But for named @@ -1024,6 +999,31 @@ pub(crate) fn check_account_existence( Ok(()) } +fn check_transfer_to_nonexisting_account( + config: &RuntimeConfig, + is_the_only_action: bool, + account_id: &AccountId, + is_refund: bool, +) -> Result<(), ActionError> { + if config.wasm_config.implicit_account_creation + && is_the_only_action + && account_id.is_implicit() + && !is_refund + { + // OK. It's implicit account creation. + // Notes: + // - The transfer action has to be the only action in the transaction to avoid + // abuse by hijacking this account with other public keys or contracts. + // - Refunds don't automatically create accounts, because refunds are free and + // we don't want some type of abuse. + // - Account deletion with beneficiary creates a refund, so it'll not create a + // new account. + Ok(()) + } else { + Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone() }.into()) + } +} + #[cfg(test)] mod tests { From 03da556105521c09980ed572ff4e7f2b6d51646b Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 26 Sep 2023 17:45:21 +0200 Subject: [PATCH 05/36] cfg everything, add mapping to action view --- chain/rosetta-rpc/Cargo.toml | 2 + chain/rosetta-rpc/src/adapters/mod.rs | 38 +++++- .../rosetta-rpc/src/adapters/transactions.rs | 2 +- core/primitives-core/Cargo.toml | 2 + core/primitives-core/src/account.rs | 114 +++++++++++----- core/primitives-core/src/version.rs | 8 +- core/primitives/Cargo.toml | 2 + core/primitives/src/action/mod.rs | 4 +- core/primitives/src/transaction.rs | 4 +- core/primitives/src/views.rs | 33 ++++- .../genesis-csv-to-json/src/csv_parser.rs | 2 +- neard/Cargo.toml | 1 + runtime/runtime/Cargo.toml | 2 + runtime/runtime/src/actions.rs | 37 +++-- runtime/runtime/src/config.rs | 21 ++- runtime/runtime/src/lib.rs | 127 +++++++++--------- runtime/runtime/src/verifier.rs | 3 +- tools/state-viewer/Cargo.toml | 2 + tools/state-viewer/src/contract_accounts.rs | 3 + 19 files changed, 271 insertions(+), 136 deletions(-) diff --git a/chain/rosetta-rpc/Cargo.toml b/chain/rosetta-rpc/Cargo.toml index 331f06d5ac0..2f7d257fdfc 100644 --- a/chain/rosetta-rpc/Cargo.toml +++ b/chain/rosetta-rpc/Cargo.toml @@ -40,6 +40,7 @@ insta.workspace = true near-actix-test-utils.workspace = true [features] +protocol_feature_nonrefundable_transfer_nep491 = [] nightly_protocol = [ "near-chain-configs/nightly_protocol", "near-client-primitives/nightly_protocol", @@ -51,6 +52,7 @@ nightly_protocol = [ ] nightly = [ "nightly_protocol", + "protocol_feature_nonrefundable_transfer_nep491", "near-chain-configs/nightly", "near-client-primitives/nightly", "near-client/nightly", diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 9e83c283df6..6c3edb8af08 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -2,7 +2,7 @@ use actix::Addr; use near_chain_configs::Genesis; use near_client::ViewClientActor; use near_o11y::WithSpanContextExt; -use near_primitives::transaction::{TransferAction, TransferActionV2}; +use near_primitives::transaction::TransferAction; use validated_operations::ValidatedOperation; mod transactions; @@ -321,11 +321,37 @@ impl From for Vec { // Note: Both refundable and non-refundable transfers are considered as available balance. // (TODO: ensure final decision for NEP-491 aligns with that!) - near_primitives::transaction::Action::Transfer(TransferAction { deposit }) - | near_primitives::transaction::Action::TransferV2(TransferActionV2 { - deposit, - .. - }) => { + near_primitives::transaction::Action::Transfer(TransferAction { deposit }) => { + let transfer_amount = crate::models::Amount::from_yoctonear(deposit); + + let sender_transfer_operation_id = + crate::models::OperationIdentifier::new(&operations); + operations.push( + validated_operations::TransferOperation { + account: sender_account_identifier.clone(), + amount: -transfer_amount.clone(), + predecessor_id: Some(sender_account_identifier.clone()), + } + .into_operation(sender_transfer_operation_id.clone()), + ); + + operations.push( + validated_operations::TransferOperation { + account: receiver_account_identifier.clone(), + amount: transfer_amount, + predecessor_id: Some(sender_account_identifier.clone()), + } + .into_related_operation( + crate::models::OperationIdentifier::new(&operations), + vec![sender_transfer_operation_id], + ), + ); + } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + near_primitives::transaction::Action::TransferV2( + near_primitives::transaction::TransferActionV2 { deposit, .. }, + ) => { + // TODO(protocol_feature_nonrefundable_transfer_nep491): merge with branch above on stabilization let transfer_amount = crate::models::Amount::from_yoctonear(deposit); let sender_transfer_operation_id = diff --git a/chain/rosetta-rpc/src/adapters/transactions.rs b/chain/rosetta-rpc/src/adapters/transactions.rs index 3589f870d0c..9e1f92dde26 100644 --- a/chain/rosetta-rpc/src/adapters/transactions.rs +++ b/chain/rosetta-rpc/src/adapters/transactions.rs @@ -261,7 +261,7 @@ pub(crate) async fn convert_block_changes_to_transactions( .actions .iter() .map(|action| match action { - near_primitives::views::ActionView::Transfer { deposit } => { + near_primitives::views::ActionView::Transfer { deposit, .. } => { *deposit } _ => 0, diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index eca9aa693f8..4330ac0e631 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -36,11 +36,13 @@ protocol_feature_fix_staking_threshold = [] protocol_feature_fix_contract_loading_cost = [] protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_simple_nightshade_v2 = [] +protocol_feature_nonrefundable_transfer_nep491 = [] nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", + "protocol_feature_nonrefundable_transfer_nep491", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_simple_nightshade_v2", ] diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 9ce3fb7347f..b06ecbff2bf 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -18,9 +18,10 @@ use std::io; serde::Deserialize, )] pub enum AccountVersion { + #[cfg_attr(not(feature = "protocol_feature_nonrefundable_transfer_nep491"), default)] V1, - // TODO(jakmeier): hide behind feature flag #[default] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] V2, } @@ -33,9 +34,9 @@ pub struct Account { /// The amount locked due to staking. #[serde(with = "dec_format")] locked: Balance, - // TODO(jakmeier): hide behind feature flag, we don't want to show this field in serde::Deserialize unless the feature is active /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. #[serde(with = "dec_format")] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable: Balance, /// Hash of the code stored in the storage for this account. code_hash: CryptoHash, @@ -47,6 +48,7 @@ pub struct Account { /// and the code doesn't allow adding a new version at all since this field /// is not included in the merklized state... #[serde(default)] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version: AccountVersion, } @@ -62,6 +64,10 @@ impl Account { pub fn new( amount: Balance, locked: Balance, + #[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + allow(unused_variables) + )] nonrefundable: Balance, code_hash: CryptoHash, storage_usage: StorageUsage, @@ -69,10 +75,11 @@ impl Account { Account { amount, locked, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, code_hash, storage_usage, - // TODO(jakmeier): condition on feature flag + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version: AccountVersion::V2, } } @@ -83,10 +90,17 @@ impl Account { } #[inline] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn nonrefundable(&self) -> Balance { self.nonrefundable } + #[inline] + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + pub fn nonrefundable(&self) -> Balance { + 0 + } + #[inline] pub fn locked(&self) -> Balance { self.locked @@ -103,6 +117,7 @@ impl Account { } #[inline] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn version(&self) -> AccountVersion { self.version } @@ -113,6 +128,8 @@ impl Account { } #[inline] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { self.nonrefundable = nonrefundable; } @@ -132,6 +149,7 @@ impl Account { self.storage_usage = storage_usage; } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn set_version(&mut self, version: AccountVersion) { self.version = version; } @@ -140,6 +158,10 @@ impl Account { /// Note(jakmeier): Even though this is called "legacy", it looks like this is /// the one and only serialization format of Accounts currently in use. #[derive(BorshSerialize)] +#[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + derive(BorshDeserialize) +)] struct LegacyAccount { amount: Balance, locked: Balance, @@ -156,14 +178,25 @@ impl BorshDeserialize for Account { // Account v2 or newer let version_byte = u8::deserialize_reader(rd)?; assert_eq!(version_byte, 2); // TODO(jakmeier): return proper error instead of panic + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let version = AccountVersion::V2; let amount = u128::deserialize_reader(rd)?; let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; let storage_usage = StorageUsage::deserialize_reader(rd)?; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let nonrefundable = u128::deserialize_reader(rd)?; - Ok(Account { amount, locked, code_hash, storage_usage, version, nonrefundable }) + Ok(Account { + amount, + locked, + code_hash, + storage_usage, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + version, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable, + }) } else { // Account v1 let locked = u128::deserialize_reader(rd)?; @@ -175,7 +208,9 @@ impl BorshDeserialize for Account { locked, code_hash, storage_usage, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version: AccountVersion::V1, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable: 0, }) } @@ -184,38 +219,45 @@ impl BorshDeserialize for Account { impl BorshSerialize for Account { fn serialize(&self, writer: &mut W) -> io::Result<()> { - match self.version { - AccountVersion::V1 => LegacyAccount { - amount: self.amount, - locked: self.locked, - code_hash: self.code_hash, - storage_usage: self.storage_usage, - // FIXME(jakmeier): can we add nonrefundable storage to existing - // accounts? IN this implementation, we burn any nonrefundable - // tokens sent to existing accounts, which is unacceptable. But - // automatically converting old V1 to V2 would break the borsh - // assumptions of unique binary representation. - } - .serialize(writer), - // TODO(jakmeier): Can we do better than this? - // Context: These accounts are serialized in merklized state. I - // would really like to avoid migration of the MPT. This here would - // keep old accounts in the old format and only allow nonrefundable - // storage on new accounts. - AccountVersion::V2 => { - let sentinel = Account::SERIALIZATION_SENTINEL; - // For now a constant, but if we need V3 later we can use this - // field instead of sentinel magic. - let version = 2u8; - BorshSerialize::serialize(&sentinel, writer)?; - BorshSerialize::serialize(&version, writer)?; - // TODO(jakmeier): Consider wrapping this in a struct and derive BorshSerialize for it. - BorshSerialize::serialize(&self.amount, writer)?; - BorshSerialize::serialize(&self.locked, writer)?; - BorshSerialize::serialize(&self.code_hash, writer)?; - BorshSerialize::serialize(&self.storage_usage, writer)?; - BorshSerialize::serialize(&self.nonrefundable, writer)?; - Ok(()) + let legacy_account = LegacyAccount { + amount: self.amount, + locked: self.locked, + code_hash: self.code_hash, + storage_usage: self.storage_usage, + }; + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + legacy_account.serialize(writer) + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + { + match self.version { + // Note: It might be tempting to lazily convert old V1 to V2 + // while serializing. But that would break the borsh assumptions + // of unique binary representation. + AccountVersion::V1 => legacy_account.serialize(writer), + // TODO(jakmeier): Can we do better than this? + // Context: These accounts are serialized in merklized state. I + // would really like to avoid migration of the MPT. This here would + // keep old accounts in the old format and only allow nonrefundable + // storage on new accounts. + AccountVersion::V2 => { + let sentinel = Account::SERIALIZATION_SENTINEL; + // For now a constant, but if we need V3 later we can use this + // field instead of sentinel magic. + let version = 2u8; + BorshSerialize::serialize(&sentinel, writer)?; + BorshSerialize::serialize(&version, writer)?; + // TODO(jakmeier): Consider wrapping this in a struct and derive BorshSerialize for it. + BorshSerialize::serialize(&self.amount, writer)?; + BorshSerialize::serialize(&self.locked, writer)?; + BorshSerialize::serialize(&self.code_hash, writer)?; + BorshSerialize::serialize(&self.storage_usage, writer)?; + BorshSerialize::serialize(&self.nonrefundable, writer)?; + Ok(()) + } } } } diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index 1c665aaf552..eb519787477 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -123,6 +123,10 @@ pub enum ProtocolFeature { /// Enables block production with post-state-root. /// NEP: https://github.com/near/NEPs/pull/507 PostStateRoot, + /// Allows creating an account with a non refundable balance to cover storage costs. + /// NEP: https://github.com/near/NEPs/pull/491 + #[cfg(feature = "protocol_feature_nonrefundable_balance_nep491")] + NonRefundableBalance, } impl ProtocolFeature { @@ -177,6 +181,8 @@ impl ProtocolFeature { #[cfg(feature = "protocol_feature_simple_nightshade_v2")] ProtocolFeature::SimpleNightshadeV2 => 135, ProtocolFeature::PostStateRoot => 136, + #[cfg(feature = "protocol_feature_nonrefundable_balance_nep491")] + ProtocolFeature::NonRefundableBalance => 140, } } } @@ -189,7 +195,7 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 63; /// 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 + 140 } else { // Enable all stable features. STABLE_PROTOCOL_VERSION diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 4334d4f3447..fd94cbc4ae4 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -49,10 +49,12 @@ protocol_feature_fix_staking_threshold = ["near-primitives-core/protocol_feature 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_nonrefundable_transfer_nep491 = [] nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", + "protocol_feature_nonrefundable_transfer_nep491", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_simple_nightshade_v2", "near-fmt/nightly", diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index 9c81621e9d7..5d1fb3bd821 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -162,7 +162,7 @@ pub struct TransferAction { serde::Serialize, serde::Deserialize, )] -// TODO(jakmeier): hide behind feature flag +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub struct TransferActionV2 { #[serde(with = "dec_format")] pub deposit: Balance, @@ -196,7 +196,7 @@ pub enum Action { DeleteKey(Box), DeleteAccount(DeleteAccountAction), Delegate(Box), - /// TODO: hide behind cfg feature + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] TransferV2(TransferActionV2), } const _: () = assert!( diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 5dcbb455786..32d54248c5b 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -11,9 +11,11 @@ use std::borrow::Borrow; use std::fmt; use std::hash::{Hash, Hasher}; +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +pub use crate::action::TransferActionV2; pub use crate::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, - DeployContractAction, FunctionCallAction, StakeAction, TransferAction, TransferActionV2, + DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; pub type LogEntry = String; diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 414b74bcdfa..6555deb9cf9 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -24,12 +24,15 @@ use crate::sharding::{ ChunkHash, ShardChunk, ShardChunkHeader, ShardChunkHeaderInner, ShardChunkHeaderInnerV2, ShardChunkHeaderV3, }; +#[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] +use crate::transaction::TransferAction; use crate::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithIdAndProof, ExecutionStatus, FunctionCallAction, PartialExecutionOutcome, PartialExecutionStatus, - SignedTransaction, StakeAction, TransferAction, + SignedTransaction, StakeAction, }; + use crate::types::{ AccountId, AccountWithPublicKey, Balance, BlockHeight, EpochHeight, EpochId, FunctionArgs, Gas, Nonce, NumBlocks, ShardId, StateChangeCause, StateChangeKind, StateChangeValue, @@ -1177,6 +1180,9 @@ pub enum ActionView { Transfer { #[serde(with = "dec_format")] deposit: Balance, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + #[cfg_attr(feature = "protocol_feature_nonrefundable_transfer_nep491", serde(default))] + nonrefundable: bool, }, Stake { #[serde(with = "dec_format")] @@ -1213,10 +1219,20 @@ impl From for ActionView { gas: action.gas, deposit: action.deposit, }, - Action::Transfer(action) => ActionView::Transfer { deposit: action.deposit }, - Action::TransferV2(..) => { - todo!("TODO(jakmeier") - } + Action::Transfer(action) => ActionView::Transfer { + deposit: action.deposit, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: false, + }, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + // TODO: We lose the information if it was a deprecated + // TransferAction or an equivalent refundable TransferActionV2. + // Is this good enough? Arguably, the view shouldn't care about it + // but this needs to be discussed with consumers of the view. + Action::TransferV2(action) => ActionView::Transfer { + deposit: action.deposit, + nonrefundable: action.nonrefundable, + }, Action::Stake(action) => { ActionView::Stake { stake: action.stake, public_key: action.public_key } } @@ -1253,7 +1269,14 @@ impl TryFrom for Action { deposit, })) } + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] ActionView::Transfer { deposit } => Action::Transfer(TransferAction { deposit }), + // TODO: We always return the new TransferActionV2. + // Is this good enough? Must the Action -> View -> Action conversion be lossless? + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + ActionView::Transfer { deposit, nonrefundable } => { + Action::TransferV2(crate::transaction::TransferActionV2 { deposit, nonrefundable }) + } ActionView::Stake { stake, public_key } => { Action::Stake(Box::new(StakeAction { stake, public_key })) } diff --git a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs index 17da184bddb..1273f02c12a 100644 --- a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs +++ b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs @@ -187,7 +187,7 @@ fn account_records(row: &Row, gas_price: Balance) -> Vec { let mut res = vec![StateRecord::Account { account_id: row.account_id.clone(), - account: Account::new(row.amount, row.validator_stake, smart_contract_hash, 0), + account: Account::new(row.amount, row.validator_stake, 0, smart_contract_hash, 0), }]; // Add restricted access keys. diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 3d775d05e52..a665ed72c52 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -90,6 +90,7 @@ nightly = [ "near-ping/nightly", "near-primitives/nightly", "near-state-parts/nightly", + "near-state-viewer/nightly", "near-store/nightly", "near-undo-block/nightly", "nearcore/nightly", diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 58e79b8abc3..713a2bff359 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -34,6 +34,7 @@ near-vm-runner.workspace = true [features] nightly = [ "nightly_protocol", + "protocol_feature_nonrefundable_transfer_nep491", "near-chain-configs/nightly", "near-o11y/nightly", "near-primitives-core/nightly", @@ -42,6 +43,7 @@ nightly = [ "near-vm-runner/nightly", ] default = [] +protocol_feature_nonrefundable_transfer_nep491 = [] nightly_protocol = [ "near-chain-configs/nightly_protocol", "near-o11y/nightly_protocol", diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 71472d8e0aa..5921e3ee824 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -18,7 +18,7 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::{ Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, - FunctionCallAction, StakeAction, TransferAction, TransferActionV2, + FunctionCallAction, StakeAction, }; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{ @@ -379,28 +379,21 @@ pub(crate) fn try_refund_allowance( pub(crate) fn action_transfer( account: &mut Account, - transfer: &TransferAction, -) -> Result<(), StorageError> { - account.set_amount(account.amount().checked_add(transfer.deposit).ok_or_else(|| { - StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) - })?); - Ok(()) -} - -pub(crate) fn action_transfer_v2( - account: &mut Account, - transfer: &TransferActionV2, + deposit: Balance, + nonrefundable: bool, ) -> Result<(), StorageError> { - if transfer.nonrefundable { - account.set_nonrefundable( - account.nonrefundable().checked_add(transfer.deposit).ok_or_else(|| { + if nonrefundable { + assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else( + || { StorageError::StorageInconsistentState( "Non-refundable account balance integer overflow".to_string(), ) - })?, - ); + }, + )?); } else { - account.set_amount(account.amount().checked_add(transfer.deposit).ok_or_else(|| { + account.set_amount(account.amount().checked_add(deposit).ok_or_else(|| { StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) })?); } @@ -900,7 +893,8 @@ pub(crate) fn check_actor_permissions( } Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), Action::Delegate(_) => (), - Action::TransferV2(_) => todo!("TODO(jakmeier"), + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Action::TransferV2(_) => (), }; Ok(()) } @@ -912,6 +906,10 @@ pub(crate) fn check_account_existence( config: &RuntimeConfig, is_the_only_action: bool, is_refund: bool, + #[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + allow(unused_variables) + )] receipt_starts_with_create_account: bool, ) -> Result<(), ActionError> { match action { @@ -950,6 +948,7 @@ pub(crate) fn check_account_existence( ); } } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] Action::TransferV2(transfer) => { if account.is_none() { return check_transfer_to_nonexisting_account( diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 9126c5cb1f8..27536b7722f 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -94,7 +94,15 @@ pub fn total_send_fees( + fees.fee(ActionCosts::function_call_byte).send_fee(sender_is_receiver) * num_bytes } - Transfer(_) | TransferV2(_) => { + Transfer(_) => { + // Account for implicit account creation + let is_receiver_implicit = + config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); + transfer_send_fee(fees, sender_is_receiver, is_receiver_implicit) + } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + TransferV2(_) => { + // Note: when stabilizing, merge with branch above // Account for implicit account creation let is_receiver_implicit = config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); @@ -186,7 +194,16 @@ pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId fees.fee(ActionCosts::function_call_base).exec_fee() + fees.fee(ActionCosts::function_call_byte).exec_fee() * num_bytes } - Transfer(_) | TransferV2(_) => { + Transfer(_) => { + // Account for implicit account creation + let is_receiver_implicit = + config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); + transfer_exec_fee(fees, is_receiver_implicit) + } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + TransferV2(_) => { + // Note: when stabilizing, merge with branch above + // Account for implicit account creation let is_receiver_implicit = config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index eba4999f596..be31888bf7f 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -24,10 +24,10 @@ use near_primitives::runtime::config::RuntimeConfig; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; -use near_primitives::transaction::ExecutionMetadata; use near_primitives::transaction::{ Action, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, }; +use near_primitives::transaction::{ExecutionMetadata, TransferAction}; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas, @@ -368,68 +368,33 @@ impl Runtime { epoch_info_provider, )?; } - Action::Transfer(transfer) => { - if let Some(account) = account.as_mut() { - action_transfer(account, transfer)?; - // Check if this is a gas refund, then try to refund the access key allowance. - if is_refund && action_receipt.signer_id == receipt.receiver_id { - try_refund_allowance( - state_update, - &receipt.receiver_id, - &action_receipt.signer_public_key, - transfer.deposit, - )?; - } - } else { - // Implicit account creation - debug_assert!(apply_state.config.wasm_config.implicit_account_creation); - debug_assert!(!is_refund); - action_implicit_account_creation_transfer( - state_update, - &apply_state.config.fees, - account, - actor_id, - &receipt.receiver_id, - transfer.deposit, - apply_state.block_height, - apply_state.current_protocol_version, - false, - ); - } + Action::Transfer(TransferAction { deposit }) => { + let nonrefundable = false; + action_transfer_or_implicit_account_creation( + account, + *deposit, + nonrefundable, + is_refund, + action_receipt, + receipt, + state_update, + apply_state, + actor_id, + )?; } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] Action::TransferV2(transfer) => { - // TOOD(jakmeier): This is just a copy-paste of `Transfer` with two lines changed, it should be refactored - if let Some(account) = account.as_mut() { - action_transfer_v2(account, transfer)?; - // Check if this is a gas refund, then try to refund the access key allowance. - if is_refund && action_receipt.signer_id == receipt.receiver_id { - try_refund_allowance( - state_update, - &receipt.receiver_id, - &action_receipt.signer_public_key, - transfer.deposit, - )?; - } - } else { - // Implicit account creation - debug_assert!(checked_feature!( - "stable", - ImplicitAccountCreation, - apply_state.current_protocol_version - )); - debug_assert!(!is_refund); - action_implicit_account_creation_transfer( - state_update, - &apply_state.config.fees, - account, - actor_id, - &receipt.receiver_id, - transfer.deposit, - apply_state.block_height, - apply_state.current_protocol_version, - transfer.nonrefundable, - ); - } + action_transfer_or_implicit_account_creation( + account, + transfer.deposit, + transfer.nonrefundable, + is_refund, + action_receipt, + receipt, + state_update, + apply_state, + actor_id, + )?; } Action::Stake(stake) => { action_stake( @@ -1554,6 +1519,46 @@ impl Runtime { } } +fn action_transfer_or_implicit_account_creation( + account: &mut Option, + deposit: u128, + nonrefundable: bool, + is_refund: bool, + action_receipt: &ActionReceipt, + receipt: &Receipt, + state_update: &mut TrieUpdate, + apply_state: &ApplyState, + actor_id: &mut AccountId, +) -> Result<(), RuntimeError> { + Ok(if let Some(account) = account.as_mut() { + action_transfer(account, deposit, nonrefundable)?; + // Check if this is a gas refund, then try to refund the access key allowance. + if is_refund && action_receipt.signer_id == receipt.receiver_id { + try_refund_allowance( + state_update, + &receipt.receiver_id, + &action_receipt.signer_public_key, + deposit, + )?; + } + } else { + // Implicit account creation + debug_assert!(apply_state.config.wasm_config.implicit_account_creation); + debug_assert!(!is_refund); + action_implicit_account_creation_transfer( + state_update, + &apply_state.config.fees, + account, + actor_id, + &receipt.receiver_id, + deposit, + apply_state.block_height, + apply_state.current_protocol_version, + nonrefundable, + ); + }) +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 29060d50224..3ab1d9981e6 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -400,7 +400,8 @@ pub fn validate_action( Action::DeployContract(a) => validate_deploy_contract_action(limit_config, a), Action::FunctionCall(a) => validate_function_call_action(limit_config, a), Action::Transfer(_) => Ok(()), - Action::TransferV2(_) => todo!("TODO(jakmeier"), + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Action::TransferV2(_) => Ok(()), Action::Stake(a) => validate_stake_action(a), Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), diff --git a/tools/state-viewer/Cargo.toml b/tools/state-viewer/Cargo.toml index d376fb2da79..b8a9459b6a4 100644 --- a/tools/state-viewer/Cargo.toml +++ b/tools/state-viewer/Cargo.toml @@ -56,8 +56,10 @@ sandbox = [ "near-chain/sandbox", "near-client/sandbox", ] +protocol_feature_nonrefundable_transfer_nep491 = [] nightly = [ "nightly_protocol", + "protocol_feature_nonrefundable_transfer_nep491", "near-chain-configs/nightly", "near-chain/nightly", "near-client/nightly", diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index cd601e4ef1e..60a568188b8 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -333,6 +333,9 @@ fn try_find_actions_spawned_by_receipt( Action::DeployContract(_) => ActionType::DeployContract, Action::FunctionCall(_) => ActionType::FunctionCall, Action::Transfer(_) => ActionType::Transfer, + #[cfg( + feature = "protocol_feature_nonrefundable_transfer_nep491" + )] Action::TransferV2(_) => ActionType::Transfer, Action::Stake(_) => ActionType::Stake, Action::AddKey(_) => ActionType::AddKey, From 256a873313f82ad30d870616acd41b821d937ff2 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 26 Sep 2023 18:11:51 +0200 Subject: [PATCH 06/36] undo cfg on account version --- core/primitives-core/src/account.rs | 35 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index b06ecbff2bf..10d2c12a045 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -25,6 +25,19 @@ pub enum AccountVersion { V2, } +impl TryFrom for AccountVersion { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(AccountVersion::V1), + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + 2 => Ok(AccountVersion::V2), + _ => Err(()), + } + } +} + /// Per account information stored in the state. #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Account { @@ -48,7 +61,6 @@ pub struct Account { /// and the code doesn't allow adding a new version at all since this field /// is not included in the merklized state... #[serde(default)] - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version: AccountVersion, } @@ -79,8 +91,7 @@ impl Account { nonrefundable, code_hash, storage_usage, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - version: AccountVersion::V2, + version: AccountVersion::default(), } } @@ -177,9 +188,10 @@ impl BorshDeserialize for Account { if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { // Account v2 or newer let version_byte = u8::deserialize_reader(rd)?; - assert_eq!(version_byte, 2); // TODO(jakmeier): return proper error instead of panic - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let version = AccountVersion::V2; + // TODO(jakmeier): return proper error instead of panic + debug_assert_eq!(version_byte, 2); + // TODO(jakmeier): return proper error instead of panic + let version = AccountVersion::try_from(version_byte).expect("TODO(jakmeier)"); let amount = u128::deserialize_reader(rd)?; let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; @@ -192,7 +204,6 @@ impl BorshDeserialize for Account { locked, code_hash, storage_usage, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, @@ -208,7 +219,6 @@ impl BorshDeserialize for Account { locked, code_hash, storage_usage, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] version: AccountVersion::V1, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable: 0, @@ -364,9 +374,14 @@ mod tests { #[test] fn test_account_serialization() { - let acc = Account::new(1_000_000, 1_000_000, CryptoHash::default(), 100); + let acc = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); let bytes = acc.try_to_vec().unwrap(); - assert_eq!(hash(&bytes).to_string(), "EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ"); + if cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491") { + expect_test::expect!("HaZPNG4KpXQ9Mre4PAA83V5usqXsA4zy4vMwSXBiBcQv") + } else { + expect_test::expect!("EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ") + } + .assert_eq(&hash(&bytes).to_string()); } #[test] From c85d091c3c421ae0ae2824066e31a4c8f0ac709d Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Tue, 26 Sep 2023 18:21:23 +0200 Subject: [PATCH 07/36] fix test compilation errors --- Cargo.lock | 1 + chain/chain/src/resharding.rs | 4 ++-- chain/rosetta-rpc/src/adapters/transactions.rs | 6 +++--- core/chain-configs/src/genesis_validate.rs | 2 +- core/primitives-core/Cargo.toml | 1 + core/primitives-core/src/account.rs | 6 ++---- integration-tests/src/tests/runtime/state_viewer.rs | 4 ++-- runtime/runtime/src/actions.rs | 7 ++++--- runtime/runtime/tests/runtime_group_tools/mod.rs | 2 +- tools/amend-genesis/src/lib.rs | 4 +++- 10 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 418e8a4956e..266dc879601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4160,6 +4160,7 @@ dependencies = [ "bs58", "derive_more", "enum-map", + "expect-test", "insta", "near-account-id", "num-rational", diff --git a/chain/chain/src/resharding.rs b/chain/chain/src/resharding.rs index 83092ad27d3..cf6b140b365 100644 --- a/chain/chain/src/resharding.rs +++ b/chain/chain/src/resharding.rs @@ -415,7 +415,7 @@ mod tests { set_account( &mut trie_update, account_id.clone(), - &Account::new(0, 0, CryptoHash::default(), 0), + &Account::new(0, 0, 0, CryptoHash::default(), 0), ); } let receipts = gen_receipts(rng, 100); @@ -469,7 +469,7 @@ mod tests { set_account( &mut trie_update, account_id.clone(), - &Account::new(0, 0, CryptoHash::default(), 0), + &Account::new(0, 0, 0, CryptoHash::default(), 0), ); } // remove accounts diff --git a/chain/rosetta-rpc/src/adapters/transactions.rs b/chain/rosetta-rpc/src/adapters/transactions.rs index 9e1f92dde26..4e13ee77dc1 100644 --- a/chain/rosetta-rpc/src/adapters/transactions.rs +++ b/chain/rosetta-rpc/src/adapters/transactions.rs @@ -261,9 +261,9 @@ pub(crate) async fn convert_block_changes_to_transactions( .actions .iter() .map(|action| match action { - near_primitives::views::ActionView::Transfer { deposit, .. } => { - *deposit - } + near_primitives::views::ActionView::Transfer { + deposit, .. + } => *deposit, _ => 0, }) .sum::(); diff --git a/core/chain-configs/src/genesis_validate.rs b/core/chain-configs/src/genesis_validate.rs index b2674cbbae2..a529d22c4df 100644 --- a/core/chain-configs/src/genesis_validate.rs +++ b/core/chain-configs/src/genesis_validate.rs @@ -204,7 +204,7 @@ mod test { const VALID_ED25519_RISTRETTO_KEY: &str = "ed25519:KuTCtARNzxZQ3YvXDeLjx83FDqxv2SdQTSbiq876zR7"; fn create_account() -> Account { - Account::new(100, 10, Default::default(), 0) + Account::new(100, 10, 0, Default::default(), 0) } #[test] diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index 4330ac0e631..9b5d4c9b82d 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -29,6 +29,7 @@ near-account-id.workspace = true [dev-dependencies] serde_json.workspace = true insta.workspace = true +expect-test.workspace = true [features] default = [] diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 10d2c12a045..7600877d05a 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -76,14 +76,12 @@ impl Account { pub fn new( amount: Balance, locked: Balance, - #[cfg_attr( - not(feature = "protocol_feature_nonrefundable_transfer_nep491"), - allow(unused_variables) - )] nonrefundable: Balance, code_hash: CryptoHash, storage_usage: StorageUsage, ) -> Self { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + assert_eq!(nonrefundable, 0); Account { amount, locked, diff --git a/integration-tests/src/tests/runtime/state_viewer.rs b/integration-tests/src/tests/runtime/state_viewer.rs index 71f33ea3291..64156511f0a 100644 --- a/integration-tests/src/tests/runtime/state_viewer.rs +++ b/integration-tests/src/tests/runtime/state_viewer.rs @@ -360,7 +360,7 @@ fn test_view_state_too_large() { set_account( &mut state_update, alice_account(), - &Account::new(0, 0, CryptoHash::default(), 50_001), + &Account::new(0, 0, 0, CryptoHash::default(), 50_001), ); let trie_viewer = TrieViewer::new(Some(50_000), None); let result = trie_viewer.view_state(&state_update, &alice_account(), b"", false); @@ -375,7 +375,7 @@ fn test_view_state_with_large_contract() { set_account( &mut state_update, alice_account(), - &Account::new(0, 0, sha256(&contract_code), 50_001), + &Account::new(0, 0, 0, sha256(&contract_code), 50_001), ); state_update.set(TrieKey::ContractCode { account_id: alice_account() }, contract_code); let trie_viewer = TrieViewer::new(Some(50_000), None); diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 5921e3ee824..9849ea33a7b 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -1144,7 +1144,7 @@ mod tests { storage_usage: u64, state_update: &mut TrieUpdate, ) -> ActionResult { - let mut account = Some(Account::new(100, 0, *code_hash, storage_usage)); + let mut account = Some(Account::new(100, 0, 0, *code_hash, storage_usage)); let mut actor_id = account_id.clone(); let mut action_result = ActionResult::default(); let receipt = Receipt::new_balance_refund(&"alice.near".parse().unwrap(), 0); @@ -1282,7 +1282,7 @@ mod tests { let tries = create_tries(); let mut state_update = tries.new_trie_update(ShardUId::single_shard(), CryptoHash::default()); - let account = Account::new(100, 0, CryptoHash::default(), 100); + let account = Account::new(100, 0, 0, CryptoHash::default(), 100); set_account(&mut state_update, account_id.clone(), &account); set_access_key(&mut state_update, account_id.clone(), public_key.clone(), access_key); @@ -1434,7 +1434,8 @@ mod tests { &sender_id, &RuntimeConfig::test(), false, - false + false, + false, ), Err(ActionErrorKind::AccountDoesNotExist { account_id: sender_id.clone() }.into()) ); diff --git a/runtime/runtime/tests/runtime_group_tools/mod.rs b/runtime/runtime/tests/runtime_group_tools/mod.rs index 4c4fba335c4..bc6a4a733a8 100644 --- a/runtime/runtime/tests/runtime_group_tools/mod.rs +++ b/runtime/runtime/tests/runtime_group_tools/mod.rs @@ -219,7 +219,7 @@ impl RuntimeGroup { if (i as u64) < num_existing_accounts { state_records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(TESTING_INIT_BALANCE, TESTING_INIT_STAKE, code_hash, 0), + account: Account::new(TESTING_INIT_BALANCE, TESTING_INIT_STAKE, 0, code_hash, 0), }); state_records.push(StateRecord::AccessKey { account_id: account_id.clone(), diff --git a/tools/amend-genesis/src/lib.rs b/tools/amend-genesis/src/lib.rs index 07d31ce4af4..6e63d43fb15 100644 --- a/tools/amend-genesis/src/lib.rs +++ b/tools/amend-genesis/src/lib.rs @@ -455,8 +455,10 @@ mod test { fn parse(&self) -> StateRecord { match &self { Self::Account { account_id, amount, locked, storage_usage } => { + // `nonrefundable_balance` can be implemented if this is required in state records. + let nonrefundable_balance = 0; let account = - Account::new(*amount, *locked, CryptoHash::default(), *storage_usage); + Account::new(*amount, *locked, nonrefundable_balance, CryptoHash::default(), *storage_usage); StateRecord::Account { account_id: account_id.parse().unwrap(), account } } Self::AccessKey { account_id, public_key } => StateRecord::AccessKey { From 1d4d27006b6ed737eab2398eb9cc6efa72448194 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 27 Sep 2023 12:23:45 +0200 Subject: [PATCH 08/36] reject new action in older protocol versions includes first integration test and TODOs for missing tests --- core/primitives-core/src/version.rs | 4 +- core/primitives/Cargo.toml | 2 +- integration-tests/Cargo.toml | 4 ++ .../src/tests/client/features.rs | 2 + .../client/features/nonrefundable_transfer.rs | 72 +++++++++++++++++++ runtime/runtime/src/verifier.rs | 20 +++++- 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 integration-tests/src/tests/client/features/nonrefundable_transfer.rs diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index eb519787477..a722a7013ef 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -125,7 +125,7 @@ pub enum ProtocolFeature { PostStateRoot, /// Allows creating an account with a non refundable balance to cover storage costs. /// NEP: https://github.com/near/NEPs/pull/491 - #[cfg(feature = "protocol_feature_nonrefundable_balance_nep491")] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] NonRefundableBalance, } @@ -181,7 +181,7 @@ impl ProtocolFeature { #[cfg(feature = "protocol_feature_simple_nightshade_v2")] ProtocolFeature::SimpleNightshadeV2 => 135, ProtocolFeature::PostStateRoot => 136, - #[cfg(feature = "protocol_feature_nonrefundable_balance_nep491")] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] ProtocolFeature::NonRefundableBalance => 140, } } diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index fd94cbc4ae4..f57866030a3 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -49,7 +49,7 @@ protocol_feature_fix_staking_threshold = ["near-primitives-core/protocol_feature 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_nonrefundable_transfer_nep491 = [] +protocol_feature_nonrefundable_transfer_nep491 = ["near-primitives-core/protocol_feature_nonrefundable_transfer_nep491"] nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 29a9e6ef6c3..81632cfbcd7 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -80,12 +80,16 @@ protocol_feature_reject_blocks_with_outdated_protocol_version = [ protocol_feature_simple_nightshade_v2 = [ "near-primitives/protocol_feature_simple_nightshade_v2", ] +protocol_feature_nonrefundable_transfer_nep491 = [ + "near-primitives/protocol_feature_nonrefundable_transfer_nep491", +] nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_simple_nightshade_v2", + "protocol_feature_nonrefundable_transfer_nep491", "near-actix-test-utils/nightly", "near-async/nightly", "near-chain-configs/nightly", diff --git a/integration-tests/src/tests/client/features.rs b/integration-tests/src/tests/client/features.rs index 744d550c8e5..595843a284a 100644 --- a/integration-tests/src/tests/client/features.rs +++ b/integration-tests/src/tests/client/features.rs @@ -15,5 +15,7 @@ mod increase_storage_compute_cost; mod limit_contract_functions_number; mod lower_storage_key_limit; mod nearvm; +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +mod nonrefundable_transfer; mod restore_receipts_after_fix_apply_chunks; mod zero_balance_account; diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs new file mode 100644 index 00000000000..226bac8fbb6 --- /dev/null +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -0,0 +1,72 @@ +//! Non-refundable transfers during account creation allow to sponsor an +//! accounts storage staking balance without that someone being able to run off +//! with the money. +//! +//! This feature introduces TransferV2 +//! +//! NEP: https://github.com/near/NEPs/pull/491 + +use crate::tests::client::utils::TestEnvNightshadeSetupExt; +use near_chain::ChainGenesis; +use near_chain_configs::Genesis; +use near_client::test_utils::TestEnv; +use near_crypto::{InMemorySigner, KeyType}; +use near_primitives::errors::{ActionsValidationError, InvalidTxError}; +use near_primitives::transaction::{Action, TransferActionV2}; +use near_primitives::types::Balance; +use near_primitives::version::{ProtocolFeature, ProtocolVersion}; +use near_primitives::views::FinalExecutionOutcomeView; +use nearcore::config::GenesisExt; + +// TODO: Test for refundable transfer V2 successfully adding balance +// TODO: Test non-refundable transfer is rejected on existing account +// TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating implicit account +// TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating named account +// TODO: Test non-refundable balance allowing to have account with zero balance and more than 1kB of state + +/// During the protocol upgrade phase, before the voting completes, we must not +/// include transaction V" actions on the chain. +/// +/// The correct way to handle it is to reject transaction before they even get +/// into the transaction pool. Hence, we check that an `InvalidTxError` error is +/// returned for older protocol versions. +#[test] +fn reject_transfer_v2_in_older_versions() { + let protocol_version = ProtocolFeature::NonRefundableBalance.protocol_version() - 1; + + let status = exec_transfer_v2(protocol_version, 1, false); + assert!( + matches!( + &status, + Err( + InvalidTxError::ActionsValidation( + ActionsValidationError::UnsupportedProtocolFeature{ protocol_feature, version } + ) + ) + if protocol_feature == "NonRefundableBalance" && *version == ProtocolFeature::NonRefundableBalance.protocol_version() + ), + "{status:?}", + ); +} + +fn exec_transfer_v2( + protocol_version: ProtocolVersion, + deposit: Balance, + nonrefundable: bool, +) -> Result { + let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); + let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + + genesis.config.protocol_version = protocol_version; + + let mut env = TestEnv::builder(ChainGenesis::test()) + .real_epoch_managers(&genesis.config) + .nightshade_runtimes(&genesis) + .build(); + + let transfer = Action::TransferV2(TransferActionV2 { deposit, nonrefundable }); + let tx = env.tx_from_actions(vec![transfer], &signer, "test1".parse().unwrap()); + + let status = env.execute_tx(tx); + status +} diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 3ab1d9981e6..654a3c45b44 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -355,7 +355,6 @@ pub(crate) fn validate_actions( let mut found_delegate_action = false; let mut iter = actions.iter().peekable(); while let Some(action) = iter.next() { - // TODO(jakmeier): make sure TransferV2 gets rejected in older protocol versions if let Action::DeleteAccount(_) = action { if iter.peek().is_some() { return Err(ActionsValidationError::DeleteActionMustBeFinal); @@ -401,7 +400,9 @@ pub fn validate_action( Action::FunctionCall(a) => validate_function_call_action(limit_config, a), Action::Transfer(_) => Ok(()), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(_) => Ok(()), + Action::TransferV2(_) => { + check_feature_enabled(ProtocolFeature::NonRefundableBalance, current_protocol_version) + } Action::Stake(a) => validate_stake_action(a), Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), @@ -531,6 +532,21 @@ fn validate_delete_action(action: &DeleteAccountAction) -> Result<(), ActionsVal Ok(()) } +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +fn check_feature_enabled( + feature: ProtocolFeature, + current_protocol_version: ProtocolVersion, +) -> Result<(), ActionsValidationError> { + if feature.protocol_version() <= current_protocol_version { + Ok(()) + } else { + Err(ActionsValidationError::UnsupportedProtocolFeature { + protocol_feature: format!("{feature:?}"), + version: feature.protocol_version(), + }) + } +} + fn truncate_string(s: &str, limit: usize) -> String { for i in (0..=limit).rev() { if let Some(s) = s.get(..i) { From 0a7fc9d5e04fe18fe3155722cd3e99f13ecfc51f Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 27 Sep 2023 17:21:57 +0200 Subject: [PATCH 09/36] add nonrefundable field to `AccountView` --- core/primitives/src/views.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 6555deb9cf9..592e02b58ca 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -65,6 +65,9 @@ pub struct AccountView { pub amount: Balance, #[serde(with = "dec_format")] pub locked: Balance, + #[serde(with = "dec_format")] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + pub nonrefundable: Balance, pub code_hash: CryptoHash, pub storage_usage: StorageUsage, /// TODO(2271): deprecated. @@ -108,6 +111,8 @@ impl From<&Account> for AccountView { AccountView { amount: account.amount(), locked: account.locked(), + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: account.nonrefundable(), code_hash: account.code_hash(), storage_usage: account.storage_usage(), storage_paid_at: 0, From b9f35abff716b83766344049d3a51c73e236f8ce Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 27 Sep 2023 17:22:36 +0200 Subject: [PATCH 10/36] fix balance checker --- core/primitives/src/action/mod.rs | 2 ++ runtime/runtime/src/balance_checker.rs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index 5d1fb3bd821..593bb6cd99b 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -215,6 +215,8 @@ impl Action { match self { Action::FunctionCall(a) => a.deposit, Action::Transfer(a) => a.deposit, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Action::TransferV2(a) => a.deposit, _ => 0, } } diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index a8da12381c0..466b4f67b2c 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -77,11 +77,11 @@ fn total_accounts_balance( accounts_ids: &HashSet, ) -> Result { accounts_ids.iter().try_fold(0u128, |accumulator, account_id| { - let (amount, locked) = match get_account(state, account_id)? { + let (amount, locked, nonrefundable) = match get_account(state, account_id)? { None => return Ok(accumulator), - Some(account) => (account.amount(), account.locked()), + Some(account) => (account.amount(), account.locked(), account.nonrefundable()), }; - Ok(safe_add_balance_apply!(accumulator, amount, locked)) + Ok(safe_add_balance_apply!(accumulator, amount, locked, nonrefundable)) }) } From 595e39f7d3ee5b58534c68de6050aedc9d1aa8de Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 27 Sep 2023 17:23:28 +0200 Subject: [PATCH 11/36] add basic transfer_v2 test --- chain/client/src/test_utils.rs | 3 + core/primitives/src/test_utils.rs | 6 ++ integration-tests/Cargo.toml | 2 +- .../client/features/nonrefundable_transfer.rs | 56 +++++++++++++++++-- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs index 998992a505a..5a79efc3efd 100644 --- a/chain/client/src/test_utils.rs +++ b/chain/client/src/test_utils.rs @@ -2237,6 +2237,9 @@ impl TestEnv { let block = self.clients[0].produce_block(tip.height + i + 1).unwrap().unwrap(); self.process_block(0, block.clone(), Provenance::PRODUCED); if let Ok(outcome) = self.clients[0].chain.get_final_transaction_result(&tx_hash) { + // process one more block to allow refunds to finish + let block = self.clients[0].produce_block(tip.height + i + 2).unwrap().unwrap(); + self.process_block(0, block.clone(), Provenance::PRODUCED); return Ok(outcome); } } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index db74e2390c2..b2e5a050d82 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -587,4 +587,10 @@ impl FinalExecutionOutcomeView { ); } } + + /// Calculates how much NEAR was burn for gas, after refunds. + pub fn gas_cost(&self) -> Balance { + self.transaction_outcome.outcome.tokens_burnt + + self.receipts_outcome.iter().map(|r| r.outcome.tokens_burnt).sum::() + } } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 81632cfbcd7..247d60235ff 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -87,9 +87,9 @@ protocol_feature_nonrefundable_transfer_nep491 = [ nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", + "protocol_feature_nonrefundable_transfer_nep491", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_simple_nightshade_v2", - "protocol_feature_nonrefundable_transfer_nep491", "near-actix-test-utils/nightly", "near-async/nightly", "near-chain-configs/nightly", diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 226bac8fbb6..f02ca6070f7 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -13,19 +13,29 @@ use near_client::test_utils::TestEnv; use near_crypto::{InMemorySigner, KeyType}; use near_primitives::errors::{ActionsValidationError, InvalidTxError}; use near_primitives::transaction::{Action, TransferActionV2}; -use near_primitives::types::Balance; +use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives::views::FinalExecutionOutcomeView; use nearcore::config::GenesisExt; -// TODO: Test for refundable transfer V2 successfully adding balance +/// Refundable transfer V2 successfully adds balance like a transfer V1. +#[test] +fn transfer_v2() { + let protocol_version = ProtocolFeature::NonRefundableBalance.protocol_version(); + exec_transfer_v2(protocol_version, 1, false) + .expect("Transfer V2 should be accepted") + .assert_success(); +} + // TODO: Test non-refundable transfer is rejected on existing account // TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating implicit account // TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating named account // TODO: Test non-refundable balance allowing to have account with zero balance and more than 1kB of state +// TODO: Test non-refundable balance cannot be transferred +// TODO: Test for deleting an account with non-refundable storage (might rip up the balance checker) /// During the protocol upgrade phase, before the voting completes, we must not -/// include transaction V" actions on the chain. +/// include transfer V2 actions on the chain. /// /// The correct way to handle it is to reject transaction before they even get /// into the transaction pool. Hence, we check that an `InvalidTxError` error is @@ -49,13 +59,27 @@ fn reject_transfer_v2_in_older_versions() { ); } +/// Sender implicitly used in all test of this module. +fn sender() -> AccountId { + "test0".parse().unwrap() +} + +/// Receiver implicitly used in all test of this module. +fn receiver() -> AccountId { + "test1".parse().unwrap() +} + +/// Creates a test environment and submits a transfer V2 action. +/// +/// This methods checks that the balance is subtracted from the sender and added +/// to the receiver, if the status was ok. No checks are done on an error. fn exec_transfer_v2( protocol_version: ProtocolVersion, deposit: Balance, nonrefundable: bool, ) -> Result { - let mut genesis = Genesis::test(vec!["test0".parse().unwrap(), "test1".parse().unwrap()], 1); - let signer = InMemorySigner::from_seed("test0".parse().unwrap(), KeyType::ED25519, "test0"); + let mut genesis = Genesis::test(vec![sender(), receiver()], 1); + let signer = InMemorySigner::from_seed(sender(), KeyType::ED25519, "test0"); genesis.config.protocol_version = protocol_version; @@ -64,9 +88,29 @@ fn exec_transfer_v2( .nightshade_runtimes(&genesis) .build(); + let sender_pre_balance = env.query_balance(sender()); + let receiver_before = env.query_account(receiver()); + let transfer = Action::TransferV2(TransferActionV2 { deposit, nonrefundable }); - let tx = env.tx_from_actions(vec![transfer], &signer, "test1".parse().unwrap()); + let tx = env.tx_from_actions(vec![transfer], &signer, receiver()); let status = env.execute_tx(tx); + + if let Ok(outcome) = &status { + let gas_cost = outcome.gas_cost(); + assert_eq!(sender_pre_balance - deposit - gas_cost, env.query_balance(sender())); + + if matches!(outcome.status, near_primitives::views::FinalExecutionStatus::SuccessValue(_)) { + let receiver_after = env.query_account(receiver()); + if nonrefundable { + assert_eq!(receiver_before.amount, receiver_after.amount); + assert_eq!(receiver_before.nonrefundable + deposit, receiver_after.nonrefundable); + } else { + assert_eq!(receiver_before.amount + deposit, receiver_after.amount); + assert_eq!(receiver_before.nonrefundable, receiver_after.nonrefundable); + } + } + } + status } From 65304e95909a4ba65701ae25135a417c66dcb3f3 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 27 Sep 2023 17:29:39 +0200 Subject: [PATCH 12/36] propagate account view field in conversion --- core/primitives/src/views.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 592e02b58ca..94916c8e655 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -128,8 +128,11 @@ impl From for AccountView { impl From<&AccountView> for Account { fn from(view: &AccountView) -> Self { - // TODO(jakmeier): expose nonrefundable storage on account view - Account::new(view.amount, view.locked, 0, view.code_hash, view.storage_usage) + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let nonrefundable = view.nonrefundable; + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let nonrefundable = 0; + Account::new(view.amount, view.locked, nonrefundable, view.code_hash, view.storage_usage) } } From aa34ee1953d7b35c37a6337e800e8a593e1ef9da Mon Sep 17 00:00:00 2001 From: Jure Bajic Date: Wed, 13 Dec 2023 20:32:40 +0100 Subject: [PATCH 13/36] Resolve merge issues --- chain/client/src/test_utils.rs | 2465 -------------------------------- chain/client/src/tests/mod.rs | 1 + runtime/runtime/src/actions.rs | 7 +- runtime/runtime/src/lib.rs | 50 +- tools/fork-network/src/cli.rs | 2 +- 5 files changed, 24 insertions(+), 2501 deletions(-) delete mode 100644 chain/client/src/test_utils.rs diff --git a/chain/client/src/test_utils.rs b/chain/client/src/test_utils.rs deleted file mode 100644 index 5a79efc3efd..00000000000 --- a/chain/client/src/test_utils.rs +++ /dev/null @@ -1,2465 +0,0 @@ -// FIXME(nagisa): Is there a good reason we're triggering this? Luckily though this is just test -// code so we're in the clear. -#![allow(clippy::arc_with_non_send_sync)] - -use std::cmp::max; -use std::collections::{HashMap, HashSet}; -use std::mem::swap; -use std::ops::DerefMut; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant}; - -use actix::{Actor, Addr, AsyncContext, Context}; -use actix_rt::{Arbiter, System}; -use chrono::DateTime; -use futures::{future, FutureExt}; -use near_async::actix::AddrWithAutoSpanContextExt; -use near_async::messaging::{CanSend, IntoSender, LateBoundSender, Sender}; -use near_async::time; -use near_chain::resharding::StateSplitRequest; -use near_chunks::shards_manager_actor::start_shards_manager; -use near_chunks::ShardsManager; -use near_epoch_manager::shard_tracker::{ShardTracker, TrackedConfig}; -use near_epoch_manager::{EpochManager, EpochManagerAdapter, EpochManagerHandle}; -use near_network::shards_manager::ShardsManagerRequestFromNetwork; -use near_primitives::errors::InvalidTxError; -use near_primitives::test_utils::create_test_signer; -use num_rational::Ratio; -use once_cell::sync::OnceCell; -use rand::{thread_rng, Rng}; -use tracing::info; - -use crate::{start_view_client, Client, ClientActor, SyncStatus, ViewClientActor}; -use chrono::Utc; -use near_chain::chain::{do_apply_chunks, BlockCatchUpRequest}; -use near_chain::state_snapshot_actor::MakeSnapshotCallback; -use near_chain::test_utils::{ - wait_for_all_blocks_in_processing, wait_for_block_in_processing, KeyValueRuntime, - MockEpochManager, ValidatorSchedule, -}; -use near_chain::types::{ChainConfig, RuntimeAdapter}; -use near_chain::{Chain, ChainGenesis, ChainStoreAccess, DoomslugThresholdMode, Provenance}; -use near_chain_configs::{ClientConfig, GenesisConfig}; -use near_chunks::adapter::ShardsManagerRequestFromClient; -use near_chunks::client::ShardsManagerResponse; -use near_chunks::test_utils::{MockClientAdapterForShardsManager, SynchronousShardsManagerAdapter}; -use near_client_primitives::types::Error; -use near_crypto::{InMemorySigner, KeyType, PublicKey, Signer}; -use near_network::test_utils::MockPeerManagerAdapter; -use near_network::types::{ - AccountOrPeerIdOrHash, HighestHeightPeerInfo, PartialEncodedChunkRequestMsg, - PartialEncodedChunkResponseMsg, PeerInfo, PeerType, -}; -use near_network::types::{BlockInfo, PeerChainInfo}; -use near_network::types::{ - ConnectedPeerInfo, FullPeerInfo, NetworkRequests, NetworkResponses, PeerManagerAdapter, -}; -use near_network::types::{ - NetworkInfo, PeerManagerMessageRequest, PeerManagerMessageResponse, SetChainInfo, -}; -use near_o11y::testonly::TracingCapture; -use near_o11y::WithSpanContextExt; -use near_primitives::action::delegate::{DelegateAction, NonDelegateAction, SignedDelegateAction}; -use near_primitives::block::{ApprovalInner, Block, GenesisId}; -use near_primitives::epoch_manager::RngSeed; -use near_primitives::hash::{hash, CryptoHash}; -use near_primitives::merkle::{merklize, MerklePath, PartialMerkleTree}; -use near_primitives::network::PeerId; -use near_primitives::receipt::Receipt; -use near_primitives::runtime::config::RuntimeConfig; -use near_primitives::shard_layout::ShardUId; -use near_primitives::sharding::{EncodedShardChunk, PartialEncodedChunk, ReedSolomonWrapper}; -use near_primitives::static_clock::StaticClock; -use near_primitives::transaction::{Action, FunctionCallAction, SignedTransaction}; - -use near_primitives::types::{ - AccountId, Balance, BlockHeight, BlockHeightDelta, EpochId, NumBlocks, NumSeats, NumShards, - ShardId, -}; -use near_primitives::utils::MaybeValidated; -use near_primitives::validator_signer::ValidatorSigner; -use near_primitives::version::{ProtocolVersion, PROTOCOL_VERSION}; -use near_primitives::views::{ - AccountView, FinalExecutionOutcomeView, QueryRequest, QueryResponseKind, StateItem, -}; -use near_store::test_utils::create_test_store; -use near_store::Store; -use near_telemetry::TelemetryActor; - -use crate::adapter::{ - AnnounceAccountRequest, BlockApproval, BlockHeadersRequest, BlockHeadersResponse, BlockRequest, - BlockResponse, ProcessTxResponse, SetNetworkInfo, StateRequestHeader, StateRequestPart, -}; - -pub struct PeerManagerMock { - handle: Box< - dyn FnMut( - PeerManagerMessageRequest, - &mut actix::Context, - ) -> PeerManagerMessageResponse, - >, -} - -impl PeerManagerMock { - fn new( - f: impl 'static - + FnMut( - PeerManagerMessageRequest, - &mut actix::Context, - ) -> PeerManagerMessageResponse, - ) -> Self { - Self { handle: Box::new(f) } - } -} - -impl actix::Actor for PeerManagerMock { - type Context = actix::Context; -} - -impl actix::Handler for PeerManagerMock { - type Result = PeerManagerMessageResponse; - fn handle(&mut self, msg: PeerManagerMessageRequest, ctx: &mut Self::Context) -> Self::Result { - (self.handle)(msg, ctx) - } -} - -impl actix::Handler for PeerManagerMock { - type Result = (); - fn handle(&mut self, _msg: SetChainInfo, _ctx: &mut Self::Context) {} -} - -/// min block production time in milliseconds -pub const MIN_BLOCK_PROD_TIME: Duration = Duration::from_millis(100); -/// max block production time in milliseconds -pub const MAX_BLOCK_PROD_TIME: Duration = Duration::from_millis(200); - -const TEST_SEED: RngSeed = [3; 32]; - -impl Client { - /// Unlike Client::start_process_block, which returns before the block finishes processing - /// This function waits until the block is processed. - /// `should_produce_chunk`: Normally, if a block is accepted, client will try to produce - /// chunks for the next block if it is the chunk producer. - /// If `should_produce_chunk` is set to false, client will skip the - /// chunk production. This is useful in tests that need to tweak - /// the produced chunk content. - fn process_block_sync_with_produce_chunk_options( - &mut self, - block: MaybeValidated, - provenance: Provenance, - should_produce_chunk: bool, - ) -> Result, near_chain::Error> { - self.start_process_block(block, provenance, Arc::new(|_| {}))?; - wait_for_all_blocks_in_processing(&mut self.chain); - let (accepted_blocks, errors) = - self.postprocess_ready_blocks(Arc::new(|_| {}), should_produce_chunk); - assert!(errors.is_empty(), "unexpected errors when processing blocks: {errors:#?}"); - Ok(accepted_blocks) - } - - pub fn process_block_test( - &mut self, - block: MaybeValidated, - provenance: Provenance, - ) -> Result, near_chain::Error> { - self.process_block_sync_with_produce_chunk_options(block, provenance, true) - } - - pub fn process_block_test_no_produce_chunk( - &mut self, - block: MaybeValidated, - provenance: Provenance, - ) -> Result, near_chain::Error> { - self.process_block_sync_with_produce_chunk_options(block, provenance, false) - } - - /// This function finishes processing all blocks that started being processed. - pub fn finish_blocks_in_processing(&mut self) -> Vec { - let mut accepted_blocks = vec![]; - while wait_for_all_blocks_in_processing(&mut self.chain) { - accepted_blocks.extend(self.postprocess_ready_blocks(Arc::new(|_| {}), true).0); - } - accepted_blocks - } - - /// This function finishes processing block with hash `hash`, if the processing of that block - /// has started. - pub fn finish_block_in_processing(&mut self, hash: &CryptoHash) -> Vec { - if let Ok(()) = wait_for_block_in_processing(&mut self.chain, hash) { - let (accepted_blocks, _) = self.postprocess_ready_blocks(Arc::new(|_| {}), true); - return accepted_blocks; - } - vec![] - } -} - -/// Sets up ClientActor and ViewClientActor viewing the same store/runtime. -pub fn setup( - vs: ValidatorSchedule, - epoch_length: BlockHeightDelta, - account_id: AccountId, - skip_sync_wait: bool, - min_block_prod_time: u64, - max_block_prod_time: u64, - enable_doomslug: bool, - archive: bool, - epoch_sync_enabled: bool, - state_sync_enabled: bool, - network_adapter: PeerManagerAdapter, - transaction_validity_period: NumBlocks, - genesis_time: DateTime, - ctx: &Context, -) -> (Block, ClientActor, Addr, ShardsManagerAdapterForTest) { - let store = create_test_store(); - let num_validator_seats = vs.all_block_producers().count() as NumSeats; - let epoch_manager = MockEpochManager::new_with_validators(store.clone(), vs, epoch_length); - let shard_tracker = ShardTracker::new_empty(epoch_manager.clone()); - let runtime = KeyValueRuntime::new_with_no_gc(store.clone(), epoch_manager.as_ref(), archive); - let chain_genesis = ChainGenesis { - time: genesis_time, - height: 0, - gas_limit: 1_000_000, - min_gas_price: 100, - max_gas_price: 1_000_000_000, - total_supply: 3_000_000_000_000_000_000_000_000_000_000_000, - gas_price_adjustment_rate: Ratio::from_integer(0), - transaction_validity_period, - epoch_length, - protocol_version: PROTOCOL_VERSION, - }; - let doomslug_threshold_mode = if enable_doomslug { - DoomslugThresholdMode::TwoThirds - } else { - DoomslugThresholdMode::NoApprovals - }; - let chain = Chain::new( - epoch_manager.clone(), - shard_tracker.clone(), - runtime.clone(), - &chain_genesis, - doomslug_threshold_mode, - ChainConfig { - save_trie_changes: true, - background_migration_threads: 1, - state_snapshot_every_n_blocks: None, - }, - None, - ) - .unwrap(); - let genesis_block = chain.get_block(&chain.genesis().hash().clone()).unwrap(); - - let signer = Arc::new(create_test_signer(account_id.as_str())); - let telemetry = TelemetryActor::default().start(); - let config = ClientConfig::test( - skip_sync_wait, - min_block_prod_time, - max_block_prod_time, - num_validator_seats, - archive, - true, - epoch_sync_enabled, - state_sync_enabled, - ); - - let adv = crate::adversarial::Controls::default(); - - let view_client_addr = start_view_client( - Some(signer.validator_id().clone()), - chain_genesis.clone(), - epoch_manager.clone(), - shard_tracker.clone(), - runtime.clone(), - network_adapter.clone(), - config.clone(), - adv.clone(), - ); - - let (shards_manager_addr, _) = start_shards_manager( - epoch_manager.clone(), - shard_tracker.clone(), - network_adapter.clone().into_sender(), - ctx.address().with_auto_span_context().into_sender(), - Some(account_id), - store, - config.chunk_request_retry_period, - ); - let shards_manager_adapter = Arc::new(shards_manager_addr); - - let client = Client::new( - config.clone(), - chain_genesis, - epoch_manager, - shard_tracker, - runtime, - network_adapter.clone(), - shards_manager_adapter.as_sender(), - Some(signer.clone()), - enable_doomslug, - TEST_SEED, - None, - ) - .unwrap(); - let client_actor = ClientActor::new( - client, - ctx.address(), - config, - PeerId::new(PublicKey::empty(KeyType::ED25519)), - network_adapter, - Some(signer), - telemetry, - ctx, - None, - adv, - None, - ) - .unwrap(); - (genesis_block, client_actor, view_client_addr, shards_manager_adapter.into()) -} - -pub fn setup_only_view( - vs: ValidatorSchedule, - epoch_length: BlockHeightDelta, - account_id: AccountId, - skip_sync_wait: bool, - min_block_prod_time: u64, - max_block_prod_time: u64, - enable_doomslug: bool, - archive: bool, - epoch_sync_enabled: bool, - state_sync_enabled: bool, - network_adapter: PeerManagerAdapter, - transaction_validity_period: NumBlocks, - genesis_time: DateTime, -) -> Addr { - let store = create_test_store(); - let num_validator_seats = vs.all_block_producers().count() as NumSeats; - let epoch_manager = MockEpochManager::new_with_validators(store.clone(), vs, epoch_length); - let shard_tracker = ShardTracker::new_empty(epoch_manager.clone()); - let runtime = KeyValueRuntime::new_with_no_gc(store, epoch_manager.as_ref(), archive); - let chain_genesis = ChainGenesis { - time: genesis_time, - height: 0, - gas_limit: 1_000_000, - min_gas_price: 100, - max_gas_price: 1_000_000_000, - total_supply: 3_000_000_000_000_000_000_000_000_000_000_000, - gas_price_adjustment_rate: Ratio::from_integer(0), - transaction_validity_period, - epoch_length, - protocol_version: PROTOCOL_VERSION, - }; - - let doomslug_threshold_mode = if enable_doomslug { - DoomslugThresholdMode::TwoThirds - } else { - DoomslugThresholdMode::NoApprovals - }; - Chain::new( - epoch_manager.clone(), - shard_tracker.clone(), - runtime.clone(), - &chain_genesis, - doomslug_threshold_mode, - ChainConfig { - save_trie_changes: true, - background_migration_threads: 1, - state_snapshot_every_n_blocks: None, - }, - None, - ) - .unwrap(); - - let signer = Arc::new(create_test_signer(account_id.as_str())); - TelemetryActor::default().start(); - let config = ClientConfig::test( - skip_sync_wait, - min_block_prod_time, - max_block_prod_time, - num_validator_seats, - archive, - true, - epoch_sync_enabled, - state_sync_enabled, - ); - - let adv = crate::adversarial::Controls::default(); - - start_view_client( - Some(signer.validator_id().clone()), - chain_genesis, - epoch_manager, - shard_tracker, - runtime, - network_adapter, - config, - adv, - ) -} - -/// Sets up ClientActor and ViewClientActor with mock PeerManager. -pub fn setup_mock( - validators: Vec, - account_id: AccountId, - skip_sync_wait: bool, - enable_doomslug: bool, - peer_manager_mock: Box< - dyn FnMut( - &PeerManagerMessageRequest, - &mut Context, - Addr, - ) -> PeerManagerMessageResponse, - >, -) -> ActorHandlesForTesting { - setup_mock_with_validity_period_and_no_epoch_sync( - validators, - account_id, - skip_sync_wait, - enable_doomslug, - peer_manager_mock, - 100, - ) -} - -pub fn setup_mock_with_validity_period_and_no_epoch_sync( - validators: Vec, - account_id: AccountId, - skip_sync_wait: bool, - enable_doomslug: bool, - mut peermanager_mock: Box< - dyn FnMut( - &PeerManagerMessageRequest, - &mut Context, - Addr, - ) -> PeerManagerMessageResponse, - >, - transaction_validity_period: NumBlocks, -) -> ActorHandlesForTesting { - let network_adapter = Arc::new(LateBoundSender::default()); - let mut vca: Option> = None; - let mut sma: Option = None; - let client_addr = ClientActor::create(|ctx: &mut Context| { - let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![validators]); - let (_, client, view_client_addr, shards_manager_adapter) = setup( - vs, - 10, - account_id, - skip_sync_wait, - MIN_BLOCK_PROD_TIME.as_millis() as u64, - MAX_BLOCK_PROD_TIME.as_millis() as u64, - enable_doomslug, - false, - false, - true, - network_adapter.clone().into(), - transaction_validity_period, - StaticClock::utc(), - ctx, - ); - vca = Some(view_client_addr); - sma = Some(shards_manager_adapter); - client - }); - let client_addr1 = client_addr.clone(); - - let network_actor = - PeerManagerMock::new(move |msg, ctx| peermanager_mock(&msg, ctx, client_addr1.clone())) - .start(); - - network_adapter.bind(network_actor); - - ActorHandlesForTesting { - client_actor: client_addr, - view_client_actor: vca.unwrap(), - shards_manager_adapter: sma.unwrap(), - } -} - -pub struct BlockStats { - hash2depth: HashMap, - num_blocks: u64, - max_chain_length: u64, - last_check: Instant, - max_divergence: u64, - last_hash: Option, - parent: HashMap, -} - -impl BlockStats { - fn new() -> BlockStats { - BlockStats { - hash2depth: HashMap::new(), - num_blocks: 0, - max_chain_length: 0, - last_check: StaticClock::instant(), - max_divergence: 0, - last_hash: None, - parent: HashMap::new(), - } - } - - fn calculate_distance(&mut self, mut lhs: CryptoHash, mut rhs: CryptoHash) -> u64 { - let mut dlhs = *self.hash2depth.get(&lhs).unwrap(); - let mut drhs = *self.hash2depth.get(&rhs).unwrap(); - - let mut result: u64 = 0; - while dlhs > drhs { - lhs = *self.parent.get(&lhs).unwrap(); - dlhs -= 1; - result += 1; - } - while dlhs < drhs { - rhs = *self.parent.get(&rhs).unwrap(); - drhs -= 1; - result += 1; - } - while lhs != rhs { - lhs = *self.parent.get(&lhs).unwrap(); - rhs = *self.parent.get(&rhs).unwrap(); - result += 2; - } - result - } - - fn add_block(&mut self, block: &Block) { - if self.hash2depth.contains_key(block.hash()) { - return; - } - let prev_height = self.hash2depth.get(block.header().prev_hash()).map(|v| *v).unwrap_or(0); - self.hash2depth.insert(*block.hash(), prev_height + 1); - self.num_blocks += 1; - self.max_chain_length = max(self.max_chain_length, prev_height + 1); - self.parent.insert(*block.hash(), *block.header().prev_hash()); - - if let Some(last_hash2) = self.last_hash { - self.max_divergence = - max(self.max_divergence, self.calculate_distance(last_hash2, *block.hash())); - } - - self.last_hash = Some(*block.hash()); - } - - pub fn check_stats(&mut self, force: bool) { - let now = StaticClock::instant(); - let diff = now.duration_since(self.last_check); - if !force && diff.lt(&Duration::from_secs(60)) { - return; - } - self.last_check = now; - let cur_ratio = (self.num_blocks as f64) / (max(1, self.max_chain_length) as f64); - info!( - "Block stats: ratio: {:.2}, num_blocks: {} max_chain_length: {} max_divergence: {}", - cur_ratio, self.num_blocks, self.max_chain_length, self.max_divergence - ); - } - - pub fn check_block_ratio(&mut self, min_ratio: Option, max_ratio: Option) { - let cur_ratio = (self.num_blocks as f64) / (max(1, self.max_chain_length) as f64); - if let Some(min_ratio2) = min_ratio { - if cur_ratio < min_ratio2 { - panic!( - "ratio of blocks to longest chain is too low got: {:.2} expected: {:.2}", - cur_ratio, min_ratio2 - ); - } - } - if let Some(max_ratio2) = max_ratio { - if cur_ratio > max_ratio2 { - panic!( - "ratio of blocks to longest chain is too high got: {:.2} expected: {:.2}", - cur_ratio, max_ratio2 - ); - } - } - } -} - -#[derive(Clone)] -pub struct ActorHandlesForTesting { - pub client_actor: Addr, - pub view_client_actor: Addr, - pub shards_manager_adapter: ShardsManagerAdapterForTest, -} - -fn send_chunks( - connectors: &[ActorHandlesForTesting], - recipients: I, - target: T, - drop_chunks: bool, - send_to: F, -) where - T: Eq, - I: Iterator, - F: Fn(&ShardsManagerAdapterForTest), -{ - for (i, name) in recipients { - if name == target { - if !drop_chunks || !thread_rng().gen_ratio(1, 5) { - send_to(&connectors[i].shards_manager_adapter); - } - } - } -} - -/// Setup multiple clients talking to each other via a mock network. -/// -/// # Arguments -/// -/// `vs` - the set of validators and how they are assigned to shards in different epochs. -/// -/// `key_pairs` - keys for `validators` -/// -/// `skip_sync_wait` -/// -/// `block_prod_time` - Minimum block production time, assuming there is enough approvals. The -/// maximum block production time depends on the value of `tamper_with_fg`, and is -/// equal to `block_prod_time` if `tamper_with_fg` is `true`, otherwise it is -/// `block_prod_time * 2` -/// -/// `drop_chunks` - if set to true, 10% of all the chunk messages / requests will be dropped -/// -/// `tamper_with_fg` - if set to true, will split the heights into groups of 100. For some groups -/// all the approvals will be dropped (thus completely disabling the finality gadget -/// and introducing severe forkfulness if `block_prod_time` is sufficiently small), -/// for some groups will keep all the approvals (and test the fg invariants), and -/// for some will drop 50% of the approvals. -/// This was designed to tamper with the finality gadget when we -/// had it, unclear if has much effect today. Must be disabled if doomslug is -/// enabled (see below), because doomslug will stall if approvals are not delivered. -/// -/// `epoch_length` - approximate length of the epoch as measured -/// by the block heights difference of it's last and first block. -/// -/// `enable_doomslug` - If false, blocks will be created when at least one approval is present, without -/// waiting for 2/3. This allows for more forkfulness. `cross_shard_tx` has modes -/// both with enabled doomslug (to test "production" setting) and with disabled -/// doomslug (to test higher forkfullness) -/// -/// `network_mock` - the callback that is called for each message sent. The `mock` is called before -/// the default processing. `mock` returns `(response, perform_default)`. If -/// `perform_default` is false, then the message is not processed or broadcasted -/// further and `response` is returned to the requester immediately. Otherwise -/// the default action is performed, that might (and likely will) overwrite the -/// `response` before it is sent back to the requester. -pub fn setup_mock_all_validators( - vs: ValidatorSchedule, - key_pairs: Vec, - skip_sync_wait: bool, - block_prod_time: u64, - drop_chunks: bool, - tamper_with_fg: bool, - epoch_length: BlockHeightDelta, - enable_doomslug: bool, - archive: Vec, - epoch_sync_enabled: Vec, - check_block_stats: bool, - peer_manager_mock: Box< - dyn FnMut( - // Peer validators - &[ActorHandlesForTesting], - // Validator that sends the message - AccountId, - // The message itself - &PeerManagerMessageRequest, - ) -> (PeerManagerMessageResponse, /* perform default */ bool), - >, -) -> (Block, Vec, Arc>) { - let peer_manager_mock = Arc::new(RwLock::new(peer_manager_mock)); - let validators = vs.all_validators().cloned().collect::>(); - let key_pairs = key_pairs; - - let addresses: Vec<_> = (0..key_pairs.len()).map(|i| hash(vec![i as u8].as_ref())).collect(); - let genesis_time = StaticClock::utc(); - let mut ret = vec![]; - - let connectors: Arc>> = Default::default(); - - let announced_accounts = Arc::new(RwLock::new(HashSet::new())); - let genesis_block = Arc::new(RwLock::new(None)); - - let last_height = Arc::new(RwLock::new(vec![0; key_pairs.len()])); - let largest_endorsed_height = Arc::new(RwLock::new(vec![0u64; key_pairs.len()])); - let largest_skipped_height = Arc::new(RwLock::new(vec![0u64; key_pairs.len()])); - let hash_to_height = Arc::new(RwLock::new(HashMap::new())); - let block_stats = Arc::new(RwLock::new(BlockStats::new())); - - for (index, account_id) in validators.clone().into_iter().enumerate() { - let vs = vs.clone(); - let block_stats1 = block_stats.clone(); - let mut view_client_addr_slot = None; - let mut shards_manager_adapter_slot = None; - let validators_clone2 = validators.clone(); - let genesis_block1 = genesis_block.clone(); - let key_pairs = key_pairs.clone(); - let key_pairs1 = key_pairs.clone(); - let addresses = addresses.clone(); - let connectors1 = connectors.clone(); - let network_mock1 = peer_manager_mock.clone(); - let announced_accounts1 = announced_accounts.clone(); - let last_height1 = last_height.clone(); - let last_height2 = last_height.clone(); - let largest_endorsed_height1 = largest_endorsed_height.clone(); - let largest_skipped_height1 = largest_skipped_height.clone(); - let hash_to_height1 = hash_to_height.clone(); - let archive1 = archive.clone(); - let epoch_sync_enabled1 = epoch_sync_enabled.clone(); - let client_addr = ClientActor::create(|ctx| { - let client_addr = ctx.address(); - let _account_id = account_id.clone(); - let pm = PeerManagerMock::new(move |msg, _ctx| { - // Note: this `.wait` will block until all `ClientActors` are created. - let connectors1 = connectors1.wait(); - let mut guard = network_mock1.write().unwrap(); - let (resp, perform_default) = - guard.deref_mut()(connectors1.as_slice(), account_id.clone(), &msg); - drop(guard); - - if perform_default { - let my_ord = validators_clone2.iter().position(|it| it == &account_id).unwrap(); - let my_key_pair = key_pairs[my_ord].clone(); - let my_address = addresses[my_ord]; - - { - let last_height2 = last_height2.read().unwrap(); - let peers: Vec<_> = key_pairs1 - .iter() - .take(connectors1.len()) - .enumerate() - .map(|(i, peer_info)| ConnectedPeerInfo { - full_peer_info: FullPeerInfo { - peer_info: peer_info.clone(), - chain_info: PeerChainInfo { - genesis_id: GenesisId { - chain_id: "unittest".to_string(), - hash: Default::default(), - }, - // TODO: add the correct hash here - last_block: Some(BlockInfo { - height: last_height2[i], - hash: CryptoHash::default(), - }), - tracked_shards: vec![], - archival: true, - }, - }, - received_bytes_per_sec: 0, - sent_bytes_per_sec: 0, - last_time_peer_requested: near_async::time::Instant::now(), - last_time_received_message: near_async::time::Instant::now(), - connection_established_time: near_async::time::Instant::now(), - peer_type: PeerType::Outbound, - nonce: 3, - }) - .collect(); - let peers2 = peers - .iter() - .filter_map(|it| it.full_peer_info.clone().into()) - .collect(); - let info = NetworkInfo { - connected_peers: peers, - tier1_connections: vec![], - num_connected_peers: key_pairs1.len(), - peer_max_count: key_pairs1.len() as u32, - highest_height_peers: peers2, - sent_bytes_per_sec: 0, - received_bytes_per_sec: 0, - known_producers: vec![], - tier1_accounts_keys: vec![], - tier1_accounts_data: vec![], - }; - client_addr.do_send(SetNetworkInfo(info).with_span_context()); - } - - match msg.as_network_requests_ref() { - NetworkRequests::Block { block } => { - if check_block_stats { - let block_stats2 = &mut *block_stats1.write().unwrap(); - block_stats2.add_block(block); - block_stats2.check_stats(false); - } - - for actor_handles in connectors1 { - actor_handles.client_actor.do_send( - BlockResponse { - block: block.clone(), - peer_id: PeerInfo::random().id, - was_requested: false, - } - .with_span_context(), - ); - } - - let mut last_height1 = last_height1.write().unwrap(); - - let my_height = &mut last_height1[my_ord]; - - *my_height = max(*my_height, block.header().height()); - - hash_to_height1 - .write() - .unwrap() - .insert(*block.header().hash(), block.header().height()); - } - NetworkRequests::PartialEncodedChunkRequest { target, request, .. } => { - send_chunks( - connectors1, - validators_clone2.iter().map(|s| Some(s.clone())).enumerate(), - target.account_id.as_ref().map(|s| s.clone()), - drop_chunks, - |c| { - c.send(ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkRequest { partial_encoded_chunk_request: request.clone(), route_back: my_address }); - }, - ); - } - NetworkRequests::PartialEncodedChunkResponse { route_back, response } => { - send_chunks( - connectors1, - addresses.iter().enumerate(), - route_back, - drop_chunks, - |c| { - c.send(ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkResponse { partial_encoded_chunk_response: response.clone(), received_time: Instant::now() }); - }, - ); - } - NetworkRequests::PartialEncodedChunkMessage { - account_id, - partial_encoded_chunk, - } => { - send_chunks( - connectors1, - validators_clone2.iter().cloned().enumerate(), - account_id.clone(), - drop_chunks, - |c| { - c.send(ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunk(partial_encoded_chunk.clone().into())); - }, - ); - } - NetworkRequests::PartialEncodedChunkForward { account_id, forward } => { - send_chunks( - connectors1, - validators_clone2.iter().cloned().enumerate(), - account_id.clone(), - drop_chunks, - |c| { - c.send(ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkForward(forward.clone())); - } - ); - } - NetworkRequests::BlockRequest { hash, peer_id } => { - for (i, peer_info) in key_pairs.iter().enumerate() { - let peer_id = peer_id.clone(); - if peer_info.id == peer_id { - let me = connectors1[my_ord].client_actor.clone(); - actix::spawn( - connectors1[i] - .view_client_actor - .send(BlockRequest(*hash).with_span_context()) - .then(move |response| { - let response = response.unwrap(); - match response { - Some(block) => { - me.do_send( - BlockResponse { - block: *block, - peer_id, - was_requested: true, - } - .with_span_context(), - ); - } - None => {} - } - future::ready(()) - }), - ); - } - } - } - NetworkRequests::BlockHeadersRequest { hashes, peer_id } => { - for (i, peer_info) in key_pairs.iter().enumerate() { - let peer_id = peer_id.clone(); - if peer_info.id == peer_id { - let me = connectors1[my_ord].client_actor.clone(); - actix::spawn( - connectors1[i] - .view_client_actor - .send( - BlockHeadersRequest(hashes.clone()) - .with_span_context(), - ) - .then(move |response| { - let response = response.unwrap(); - match response { - Some(headers) => { - me.do_send( - BlockHeadersResponse(headers, peer_id) - .with_span_context(), - ); - } - None => {} - } - future::ready(()) - }), - ); - } - } - } - NetworkRequests::StateRequestHeader { - shard_id, - sync_hash, - target: target_account_id, - } => { - let target_account_id = match target_account_id { - AccountOrPeerIdOrHash::AccountId(x) => x, - _ => panic!(), - }; - for (i, name) in validators_clone2.iter().enumerate() { - if name == target_account_id { - let me = connectors1[my_ord].client_actor.clone(); - actix::spawn( - connectors1[i] - .view_client_actor - .send( - StateRequestHeader { - shard_id: *shard_id, - sync_hash: *sync_hash, - } - .with_span_context(), - ) - .then(move |response| { - let response = response.unwrap(); - match response { - Some(response) => { - me.do_send(response.with_span_context()); - } - None => {} - } - future::ready(()) - }), - ); - } - } - } - NetworkRequests::StateRequestPart { - shard_id, - sync_hash, - part_id, - target: target_account_id, - } => { - let target_account_id = match target_account_id { - AccountOrPeerIdOrHash::AccountId(x) => x, - _ => panic!(), - }; - for (i, name) in validators_clone2.iter().enumerate() { - if name == target_account_id { - let me = connectors1[my_ord].client_actor.clone(); - actix::spawn( - connectors1[i] - .view_client_actor - .send( - StateRequestPart { - shard_id: *shard_id, - sync_hash: *sync_hash, - part_id: *part_id, - } - .with_span_context(), - ) - .then(move |response| { - let response = response.unwrap(); - match response { - Some(response) => { - me.do_send(response.with_span_context()); - } - None => {} - } - future::ready(()) - }), - ); - } - } - } - NetworkRequests::AnnounceAccount(announce_account) => { - let mut aa = announced_accounts1.write().unwrap(); - let key = ( - announce_account.account_id.clone(), - announce_account.epoch_id.clone(), - ); - if aa.get(&key).is_none() { - aa.insert(key); - for actor_handles in connectors1 { - actor_handles.view_client_actor.do_send( - AnnounceAccountRequest(vec![( - announce_account.clone(), - None, - )]) - .with_span_context(), - ) - } - } - } - NetworkRequests::Approval { approval_message } => { - let height_mod = approval_message.approval.target_height % 300; - - let do_propagate = if tamper_with_fg { - if height_mod < 100 { - false - } else if height_mod < 200 { - let mut rng = rand::thread_rng(); - rng.gen() - } else { - true - } - } else { - true - }; - - let approval = approval_message.approval.clone(); - - if do_propagate { - for (i, name) in validators_clone2.iter().enumerate() { - if name == &approval_message.target { - connectors1[i].client_actor.do_send( - BlockApproval(approval.clone(), my_key_pair.id.clone()) - .with_span_context(), - ); - } - } - } - - // Verify doomslug invariant - match approval.inner { - ApprovalInner::Endorsement(parent_hash) => { - assert!( - approval.target_height - > largest_skipped_height1.read().unwrap()[my_ord] - ); - largest_endorsed_height1.write().unwrap()[my_ord] = - approval.target_height; - - if let Some(prev_height) = - hash_to_height1.read().unwrap().get(&parent_hash) - { - assert_eq!(prev_height + 1, approval.target_height); - } - } - ApprovalInner::Skip(prev_height) => { - largest_skipped_height1.write().unwrap()[my_ord] = - approval.target_height; - let e = largest_endorsed_height1.read().unwrap()[my_ord]; - // `e` is the *target* height of the last endorsement. `prev_height` - // is allowed to be anything >= to the source height, which is e-1. - assert!( - prev_height + 1 >= e, - "New: {}->{}, Old: {}->{}", - prev_height, - approval.target_height, - e - 1, - e - ); - } - }; - } - NetworkRequests::ForwardTx(_, _) - | NetworkRequests::BanPeer { .. } - | NetworkRequests::TxStatus(_, _, _) - | NetworkRequests::Challenge(_) => {} - }; - } - resp - }) - .start(); - let (block, client, view_client_addr, shards_manager_adapter) = setup( - vs, - epoch_length, - _account_id, - skip_sync_wait, - block_prod_time, - block_prod_time * 3, - enable_doomslug, - archive1[index], - epoch_sync_enabled1[index], - true, - Arc::new(pm).into(), - 10000, - genesis_time, - ctx, - ); - view_client_addr_slot = Some(view_client_addr); - shards_manager_adapter_slot = Some(shards_manager_adapter); - *genesis_block1.write().unwrap() = Some(block); - client - }); - ret.push(ActorHandlesForTesting { - client_actor: client_addr, - view_client_actor: view_client_addr_slot.unwrap(), - shards_manager_adapter: shards_manager_adapter_slot.unwrap(), - }); - } - hash_to_height.write().unwrap().insert(CryptoHash::default(), 0); - hash_to_height - .write() - .unwrap() - .insert(*genesis_block.read().unwrap().as_ref().unwrap().header().clone().hash(), 0); - connectors.set(ret.clone()).ok().unwrap(); - let value = genesis_block.read().unwrap(); - (value.clone().unwrap(), ret, block_stats) -} - -/// Sets up ClientActor and ViewClientActor without network. -pub fn setup_no_network( - validators: Vec, - account_id: AccountId, - skip_sync_wait: bool, - enable_doomslug: bool, -) -> ActorHandlesForTesting { - setup_no_network_with_validity_period_and_no_epoch_sync( - validators, - account_id, - skip_sync_wait, - 100, - enable_doomslug, - ) -} - -pub fn setup_no_network_with_validity_period_and_no_epoch_sync( - validators: Vec, - account_id: AccountId, - skip_sync_wait: bool, - transaction_validity_period: NumBlocks, - enable_doomslug: bool, -) -> ActorHandlesForTesting { - setup_mock_with_validity_period_and_no_epoch_sync( - validators, - account_id, - skip_sync_wait, - enable_doomslug, - Box::new(|_, _, _| { - PeerManagerMessageResponse::NetworkResponses(NetworkResponses::NoResponse) - }), - transaction_validity_period, - ) -} - -pub fn setup_client_with_runtime( - num_validator_seats: NumSeats, - account_id: Option, - enable_doomslug: bool, - network_adapter: PeerManagerAdapter, - shards_manager_adapter: ShardsManagerAdapterForTest, - chain_genesis: ChainGenesis, - epoch_manager: Arc, - shard_tracker: ShardTracker, - runtime: Arc, - rng_seed: RngSeed, - archive: bool, - save_trie_changes: bool, - make_state_snapshot_callback: Option, -) -> Client { - let validator_signer = - account_id.map(|x| Arc::new(create_test_signer(x.as_str())) as Arc); - let mut config = ClientConfig::test( - true, - 10, - 20, - num_validator_seats, - archive, - save_trie_changes, - true, - true, - ); - config.epoch_length = chain_genesis.epoch_length; - let mut client = Client::new( - config, - chain_genesis, - epoch_manager, - shard_tracker, - runtime, - network_adapter, - shards_manager_adapter.client, - validator_signer, - enable_doomslug, - rng_seed, - make_state_snapshot_callback, - ) - .unwrap(); - client.sync_status = SyncStatus::NoSync; - client -} - -pub fn setup_client( - store: Store, - vs: ValidatorSchedule, - account_id: Option, - enable_doomslug: bool, - network_adapter: PeerManagerAdapter, - shards_manager_adapter: ShardsManagerAdapterForTest, - chain_genesis: ChainGenesis, - rng_seed: RngSeed, - archive: bool, - save_trie_changes: bool, -) -> Client { - let num_validator_seats = vs.all_block_producers().count() as NumSeats; - let epoch_manager = - MockEpochManager::new_with_validators(store.clone(), vs, chain_genesis.epoch_length); - let shard_tracker = ShardTracker::new_empty(epoch_manager.clone()); - let runtime = KeyValueRuntime::new(store, epoch_manager.as_ref()); - setup_client_with_runtime( - num_validator_seats, - account_id, - enable_doomslug, - network_adapter, - shards_manager_adapter, - chain_genesis, - epoch_manager, - shard_tracker, - runtime, - rng_seed, - archive, - save_trie_changes, - None, - ) -} - -pub fn setup_synchronous_shards_manager( - account_id: Option, - client_adapter: Sender, - network_adapter: PeerManagerAdapter, - epoch_manager: Arc, - shard_tracker: ShardTracker, - runtime: Arc, - chain_genesis: &ChainGenesis, -) -> ShardsManagerAdapterForTest { - // Initialize the chain, to make sure that if the store is empty, we write the genesis - // into the store, and as a short cut to get the parameters needed to instantiate - // ShardsManager. This way we don't have to wait to construct the Client first. - // TODO(#8324): This should just be refactored so that we can construct Chain first - // before anything else. - let chain = Chain::new( - epoch_manager.clone(), - shard_tracker.clone(), - runtime, - chain_genesis, - DoomslugThresholdMode::TwoThirds, // irrelevant - ChainConfig { - save_trie_changes: true, - background_migration_threads: 1, - state_snapshot_every_n_blocks: None, - }, // irrelevant - None, - ) - .unwrap(); - let chain_head = chain.head().unwrap(); - let chain_header_head = chain.header_head().unwrap(); - let shards_manager = ShardsManager::new( - time::Clock::real(), - account_id, - epoch_manager, - shard_tracker, - network_adapter.request_sender, - client_adapter, - chain.store().new_read_only_chunks_store(), - chain_head, - chain_header_head, - ); - Arc::new(SynchronousShardsManagerAdapter::new(shards_manager)).into() -} - -pub fn setup_client_with_synchronous_shards_manager( - store: Store, - vs: ValidatorSchedule, - account_id: Option, - enable_doomslug: bool, - network_adapter: PeerManagerAdapter, - client_adapter: Sender, - chain_genesis: ChainGenesis, - rng_seed: RngSeed, - archive: bool, - save_trie_changes: bool, -) -> Client { - let num_validator_seats = vs.all_block_producers().count() as NumSeats; - let epoch_manager = - MockEpochManager::new_with_validators(store.clone(), vs, chain_genesis.epoch_length); - let shard_tracker = ShardTracker::new_empty(epoch_manager.clone()); - let runtime = KeyValueRuntime::new(store, epoch_manager.as_ref()); - let shards_manager_adapter = setup_synchronous_shards_manager( - account_id.clone(), - client_adapter, - network_adapter.clone(), - epoch_manager.clone(), - shard_tracker.clone(), - runtime.clone(), - &chain_genesis, - ); - setup_client_with_runtime( - num_validator_seats, - account_id, - enable_doomslug, - network_adapter, - shards_manager_adapter, - chain_genesis, - epoch_manager, - shard_tracker, - runtime, - rng_seed, - archive, - save_trie_changes, - None, - ) -} - -/// A combined trait bound for both the client side and network side of the ShardsManager API. -#[derive(Clone, derive_more::AsRef)] -pub struct ShardsManagerAdapterForTest { - pub client: Sender, - pub network: Sender, -} - -impl + CanSend> - From> for ShardsManagerAdapterForTest -{ - fn from(arc: Arc) -> Self { - Self { client: arc.as_sender(), network: arc.as_sender() } - } -} - -/// An environment for writing integration tests with multiple clients. -/// This environment can simulate near nodes without network and it can be configured to use different runtimes. -pub struct TestEnv { - pub chain_genesis: ChainGenesis, - pub validators: Vec, - pub network_adapters: Vec>, - pub client_adapters: Vec>, - pub shards_manager_adapters: Vec, - pub clients: Vec, - account_to_client_index: HashMap, - paused_blocks: Arc>>>>, - // random seed to be inject in each client according to AccountId - // if not set, a default constant TEST_SEED will be injected - seeds: HashMap, - archive: bool, - save_trie_changes: bool, -} - -#[derive(derive_more::From, Clone)] -enum EpochManagerKind { - Mock(Arc), - Handle(Arc), -} - -impl EpochManagerKind { - pub fn into_adapter(self) -> Arc { - match self { - Self::Mock(mock) => mock, - Self::Handle(handle) => handle, - } - } -} - -/// A builder for the TestEnv structure. -pub struct TestEnvBuilder { - chain_genesis: ChainGenesis, - clients: Vec, - validators: Vec, - stores: Option>, - epoch_managers: Option>, - shard_trackers: Option>, - runtimes: Option>>, - network_adapters: Option>>, - num_shards: Option, - // random seed to be inject in each client according to AccountId - // if not set, a default constant TEST_SEED will be injected - seeds: HashMap, - archive: bool, - save_trie_changes: bool, - add_state_snapshots: bool, -} - -/// Builder for the [`TestEnv`] structure. -impl TestEnvBuilder { - /// Constructs a new builder. - fn new(chain_genesis: ChainGenesis) -> Self { - let clients = Self::make_accounts(1); - let validators = clients.clone(); - let seeds: HashMap = HashMap::with_capacity(1); - Self { - chain_genesis, - clients, - validators, - stores: None, - epoch_managers: None, - shard_trackers: None, - runtimes: None, - network_adapters: None, - num_shards: None, - seeds, - archive: false, - save_trie_changes: true, - add_state_snapshots: false, - } - } - - /// Sets list of client [`AccountId`]s to the one provided. Panics if the - /// vector is empty. - pub fn clients(mut self, clients: Vec) -> Self { - assert!(!clients.is_empty()); - assert!(self.stores.is_none(), "Cannot set clients after stores"); - assert!(self.epoch_managers.is_none(), "Cannot set clients after epoch_managers"); - assert!(self.shard_trackers.is_none(), "Cannot set clients after shard_trackers"); - assert!(self.runtimes.is_none(), "Cannot set clients after runtimes"); - assert!(self.network_adapters.is_none(), "Cannot set clients after network_adapters"); - self.clients = clients; - self - } - - /// Sets random seed for each client according to the provided HashMap. - pub fn clients_random_seeds(mut self, seeds: HashMap) -> Self { - self.seeds = seeds; - self - } - - /// Sets number of clients to given one. To get [`AccountId`] used by the - /// validator associated with the client the [`TestEnv::get_client_id`] - /// method can be used. Tests should not rely on any particular format of - /// account identifiers used by the builder. Panics if `num` is zero. - pub fn clients_count(self, num: usize) -> Self { - self.clients(Self::make_accounts(num)) - } - - /// Sets list of validator [`AccountId`]s to the one provided. Panics if - /// the vector is empty. - pub fn validators(mut self, validators: Vec) -> Self { - assert!(!validators.is_empty()); - assert!(self.epoch_managers.is_none(), "Cannot set validators after epoch_managers"); - self.validators = validators; - self - } - - /// Sets number of validator seats to given one. To get [`AccountId`] used - /// in the test environment the `validators` field of the built [`TestEnv`] - /// object can be used. Tests should not rely on any particular format of - /// account identifiers used by the builder. Panics if `num` is zero. - pub fn validator_seats(self, num: usize) -> Self { - self.validators(Self::make_accounts(num)) - } - - /// Overrides the stores that are used to create epoch managers and runtimes. - pub fn stores(mut self, stores: Vec) -> Self { - assert_eq!(stores.len(), self.clients.len()); - assert!(self.stores.is_none(), "Cannot override twice"); - assert!(self.epoch_managers.is_none(), "Cannot override store after epoch_managers"); - assert!(self.runtimes.is_none(), "Cannot override store after runtimes"); - self.stores = Some(stores); - self - } - - /// Internal impl to make sure the stores are initialized. - fn ensure_stores(self) -> Self { - if self.stores.is_some() { - self - } else { - let num_clients = self.clients.len(); - self.stores((0..num_clients).map(|_| create_test_store()).collect()) - } - } - - /// Specifies custom MockEpochManager for each client. This allows us to - /// construct [`TestEnv`] with a custom implementation. - /// - /// The vector must have the same number of elements as they are clients - /// (one by default). If that does not hold, [`Self::build`] method will - /// panic. - pub fn mock_epoch_managers(mut self, epoch_managers: Vec>) -> Self { - assert_eq!(epoch_managers.len(), self.clients.len()); - assert!(self.epoch_managers.is_none(), "Cannot override twice"); - assert!( - self.num_shards.is_none(), - "Cannot set both num_shards and epoch_managers at the same time" - ); - assert!( - self.shard_trackers.is_none(), - "Cannot override epoch_managers after shard_trackers" - ); - assert!(self.runtimes.is_none(), "Cannot override epoch_managers after runtimes"); - self.epoch_managers = - Some(epoch_managers.into_iter().map(|epoch_manager| epoch_manager.into()).collect()); - self - } - - /// Specifies custom EpochManagerHandle for each client. This allows us to - /// construct [`TestEnv`] with a custom implementation. - /// - /// The vector must have the same number of elements as they are clients - /// (one by default). If that does not hold, [`Self::build`] method will - /// panic. - pub fn epoch_managers(mut self, epoch_managers: Vec>) -> Self { - assert_eq!(epoch_managers.len(), self.clients.len()); - assert!(self.epoch_managers.is_none(), "Cannot override twice"); - assert!( - self.num_shards.is_none(), - "Cannot set both num_shards and epoch_managers at the same time" - ); - assert!( - self.shard_trackers.is_none(), - "Cannot override epoch_managers after shard_trackers" - ); - assert!(self.runtimes.is_none(), "Cannot override epoch_managers after runtimes"); - self.epoch_managers = - Some(epoch_managers.into_iter().map(|epoch_manager| epoch_manager.into()).collect()); - self - } - - /// Constructs real EpochManager implementations for each instance. - pub fn real_epoch_managers(self, genesis_config: &GenesisConfig) -> Self { - assert!( - self.num_shards.is_none(), - "Cannot set both num_shards and epoch_managers at the same time" - ); - let ret = self.ensure_stores(); - let epoch_managers = (0..ret.clients.len()) - .map(|i| { - EpochManager::new_arc_handle( - ret.stores.as_ref().unwrap()[i].clone(), - genesis_config, - ) - }) - .collect(); - ret.epoch_managers(epoch_managers) - } - - /// Internal impl to make sure EpochManagers are initialized. - fn ensure_epoch_managers(self) -> Self { - let mut ret = self.ensure_stores(); - if ret.epoch_managers.is_some() { - ret - } else { - let epoch_managers: Vec = (0..ret.clients.len()) - .map(|i| { - let vs = ValidatorSchedule::new_with_shards(ret.num_shards.unwrap_or(1)) - .block_producers_per_epoch(vec![ret.validators.clone()]); - MockEpochManager::new_with_validators( - ret.stores.as_ref().unwrap()[i].clone(), - vs, - ret.chain_genesis.epoch_length, - ) - .into() - }) - .collect(); - assert!( - ret.shard_trackers.is_none(), - "Cannot override shard_trackers without overriding epoch_managers" - ); - assert!( - ret.runtimes.is_none(), - "Cannot override runtimes without overriding epoch_managers" - ); - ret.epoch_managers = Some(epoch_managers); - ret - } - } - - /// Visible for extension methods in integration-tests. - pub fn internal_ensure_epoch_managers_for_nightshade_runtime( - self, - ) -> (Self, Vec, Vec>) { - let builder = self.ensure_epoch_managers(); - let stores = builder.stores.clone().unwrap(); - let epoch_managers = builder - .epoch_managers - .clone() - .unwrap() - .into_iter() - .map(|kind| match kind { - EpochManagerKind::Mock(_) => { - panic!("NightshadeRuntime can only be instantiated with EpochManagerHandle") - } - EpochManagerKind::Handle(handle) => handle, - }) - .collect(); - (builder, stores, epoch_managers) - } - - /// Specifies custom ShardTracker for each client. This allows us to - /// construct [`TestEnv`] with a custom implementation. - pub fn shard_trackers(mut self, shard_trackers: Vec) -> Self { - assert_eq!(shard_trackers.len(), self.clients.len()); - assert!(self.shard_trackers.is_none(), "Cannot override twice"); - self.shard_trackers = Some(shard_trackers); - self - } - - /// Constructs ShardTracker that tracks all shards for each instance. - /// - /// Note that in order to track *NO* shards, just don't override shard_trackers. - pub fn track_all_shards(self) -> Self { - let ret = self.ensure_epoch_managers(); - let shard_trackers = ret - .epoch_managers - .as_ref() - .unwrap() - .iter() - .map(|epoch_manager| { - ShardTracker::new(TrackedConfig::AllShards, epoch_manager.clone().into_adapter()) - }) - .collect(); - ret.shard_trackers(shard_trackers) - } - - /// Internal impl to make sure ShardTrackers are initialized. - fn ensure_shard_trackers(self) -> Self { - let ret = self.ensure_epoch_managers(); - if ret.shard_trackers.is_some() { - ret - } else { - let shard_trackers = ret - .epoch_managers - .as_ref() - .unwrap() - .iter() - .map(|epoch_manager| { - ShardTracker::new( - TrackedConfig::new_empty(), - epoch_manager.clone().into_adapter(), - ) - }) - .collect(); - ret.shard_trackers(shard_trackers) - } - } - - /// Specifies custom RuntimeAdapter for each client. This allows us to - /// construct [`TestEnv`] with a custom implementation. - pub fn runtimes(mut self, runtimes: Vec>) -> Self { - assert_eq!(runtimes.len(), self.clients.len()); - assert!(self.runtimes.is_none(), "Cannot override twice"); - self.runtimes = Some(runtimes); - self - } - - /// Internal impl to make sure runtimes are initialized. - fn ensure_runtimes(self) -> Self { - let ret = self.ensure_epoch_managers(); - if ret.runtimes.is_some() { - ret - } else { - let runtimes = (0..ret.clients.len()) - .map(|i| { - let epoch_manager = match &ret.epoch_managers.as_ref().unwrap()[i] { - EpochManagerKind::Mock(mock) => mock.as_ref(), - EpochManagerKind::Handle(_) => { - panic!( - "Can only default construct KeyValueRuntime with MockEpochManager" - ) - } - }; - KeyValueRuntime::new(ret.stores.as_ref().unwrap()[i].clone(), epoch_manager) - as Arc - }) - .collect(); - ret.runtimes(runtimes) - } - } - - /// Specifies custom network adaptors for each client. - /// - /// The vector must have the same number of elements as they are clients - /// (one by default). If that does not hold, [`Self::build`] method will - /// panic. - pub fn network_adapters(mut self, adapters: Vec>) -> Self { - self.network_adapters = Some(adapters); - self - } - - /// Internal impl to make sure network adapters are initialized. - fn ensure_network_adapters(self) -> Self { - if self.network_adapters.is_some() { - self - } else { - let num_clients = self.clients.len(); - self.network_adapters((0..num_clients).map(|_| Arc::new(Default::default())).collect()) - } - } - - pub fn num_shards(mut self, num_shards: NumShards) -> Self { - assert!( - self.epoch_managers.is_none(), - "Cannot set both num_shards and epoch_managers at the same time" - ); - self.num_shards = Some(num_shards); - self - } - - pub fn archive(mut self, archive: bool) -> Self { - self.archive = archive; - self - } - - pub fn save_trie_changes(mut self, save_trie_changes: bool) -> Self { - self.save_trie_changes = save_trie_changes; - self - } - - /// Constructs new `TestEnv` structure. - /// - /// If no clients were configured (either through count or vector) one - /// client is created. Similarly, if no validator seats were configured, - /// one seat is configured. - /// - /// Panics if `runtime_adapters` or `network_adapters` methods were used and - /// the length of the vectors passed to them did not equal number of - /// configured clients. - pub fn build(self) -> TestEnv { - self.ensure_shard_trackers().ensure_runtimes().ensure_network_adapters().build_impl() - } - - fn build_impl(self) -> TestEnv { - let chain_genesis = self.chain_genesis; - let clients = self.clients.clone(); - let num_clients = clients.len(); - let validators = self.validators; - let num_validators = validators.len(); - let seeds = self.seeds; - let epoch_managers = self.epoch_managers.unwrap(); - let shard_trackers = self.shard_trackers.unwrap(); - let runtimes = self.runtimes.unwrap(); - let network_adapters = self.network_adapters.unwrap(); - let client_adapters = (0..num_clients) - .map(|_| Arc::new(MockClientAdapterForShardsManager::default())) - .collect::>(); - let shards_manager_adapters = (0..num_clients) - .map(|i| { - let epoch_manager = epoch_managers[i].clone(); - let shard_tracker = shard_trackers[i].clone(); - let runtime = runtimes[i].clone(); - let network_adapter = network_adapters[i].clone(); - let client_adapter = client_adapters[i].clone(); - setup_synchronous_shards_manager( - Some(clients[i].clone()), - client_adapter.as_sender(), - network_adapter.into(), - epoch_manager.into_adapter(), - shard_tracker, - runtime, - &chain_genesis, - ) - }) - .collect::>(); - let clients = (0..num_clients) - .map(|i| { - let account_id = clients[i].clone(); - let network_adapter = network_adapters[i].clone(); - let shards_manager_adapter = shards_manager_adapters[i].clone(); - let epoch_manager = epoch_managers[i].clone(); - let shard_tracker = shard_trackers[i].clone(); - let runtime = runtimes[i].clone(); - let rng_seed = match seeds.get(&account_id) { - Some(seed) => *seed, - None => TEST_SEED, - }; - let make_state_snapshot_callback : Option = if self.add_state_snapshots { - let runtime = runtime.clone(); - let snapshot : MakeSnapshotCallback = Arc::new(move |prev_block_hash, shard_uids, block| { - tracing::info!(target: "state_snapshot", ?prev_block_hash, "make_snapshot_callback"); - runtime.get_tries().make_state_snapshot(&prev_block_hash, &shard_uids, &block).unwrap(); - }); - Some(snapshot) - } else { - None - }; - setup_client_with_runtime( - u64::try_from(num_validators).unwrap(), - Some(account_id), - false, - network_adapter.into(), - shards_manager_adapter, - chain_genesis.clone(), - epoch_manager.into_adapter(), - shard_tracker, - runtime, - rng_seed, - self.archive, - self.save_trie_changes, - make_state_snapshot_callback, - ) - }) - .collect(); - - TestEnv { - chain_genesis, - validators, - network_adapters, - client_adapters, - shards_manager_adapters, - clients, - account_to_client_index: self - .clients - .into_iter() - .enumerate() - .map(|(index, client)| (client, index)) - .collect(), - paused_blocks: Default::default(), - seeds, - archive: self.archive, - save_trie_changes: self.save_trie_changes, - } - } - - fn make_accounts(count: usize) -> Vec { - (0..count).map(|i| format!("test{}", i).parse().unwrap()).collect() - } - - pub fn use_state_snapshots(mut self) -> Self { - self.add_state_snapshots = true; - self - } -} - -impl TestEnv { - pub fn builder(chain_genesis: ChainGenesis) -> TestEnvBuilder { - TestEnvBuilder::new(chain_genesis) - } - - /// Process a given block in the client with index `id`. - /// Simulate the block processing logic in `Client`, i.e, it would run catchup and then process accepted blocks and possibly produce chunks. - pub fn process_block(&mut self, id: usize, block: Block, provenance: Provenance) { - self.clients[id].process_block_test(MaybeValidated::from(block), provenance).unwrap(); - } - - /// Produces block by given client, which may kick off chunk production. - /// This means that transactions added before this call will be included in the next block produced by this validator. - pub fn produce_block(&mut self, id: usize, height: BlockHeight) { - let block = self.clients[id].produce_block(height).unwrap(); - self.process_block(id, block.unwrap(), Provenance::PRODUCED); - } - - /// Pause processing of the given block, which means that the background - /// thread which applies the chunks on the block will get blocked until - /// `resume_block_processing` is called. - /// - /// Note that you must call `resume_block_processing` at some later point to - /// unstuck the block. - /// - /// Implementation is rather crude and just hijacks our logging - /// infrastructure. Hopefully this is good enough, but, if it isn't, we can - /// add something more robust. - pub fn pause_block_processing(&mut self, capture: &mut TracingCapture, block: &CryptoHash) { - let paused_blocks = Arc::clone(&self.paused_blocks); - paused_blocks.lock().unwrap().insert(*block, Arc::new(OnceCell::new())); - capture.set_callback(move |msg| { - if msg.starts_with("do_apply_chunks") { - let cell = paused_blocks.lock().unwrap().iter().find_map(|(block_hash, cell)| { - if msg.contains(&format!("block_hash={block_hash}")) { - Some(Arc::clone(cell)) - } else { - None - } - }); - if let Some(cell) = cell { - cell.wait(); - } - } - }); - } - - /// See `pause_block_processing`. - pub fn resume_block_processing(&mut self, block: &CryptoHash) { - let mut paused_blocks = self.paused_blocks.lock().unwrap(); - let cell = paused_blocks.remove(block).unwrap(); - let _ = cell.set(()); - } - - pub fn client(&mut self, account_id: &AccountId) -> &mut Client { - &mut self.clients[self.account_to_client_index[account_id]] - } - - pub fn shards_manager(&self, account: &AccountId) -> &ShardsManagerAdapterForTest { - &self.shards_manager_adapters[self.account_to_client_index[account]] - } - - pub fn process_partial_encoded_chunks(&mut self) { - let network_adapters = self.network_adapters.clone(); - - let mut keep_going = true; - while keep_going { - for network_adapter in network_adapters.iter() { - keep_going = false; - // process partial encoded chunks - while let Some(request) = network_adapter.pop() { - // if there are any requests in any of the adapters reset - // keep going to true as processing of any message may - // trigger more messages to be processed in other clients - // it's a bit sad and it would be much nicer if all messages - // were forwarded to a single queue - // TODO would be nicer to first handle all PECs and then all PECFs - keep_going = true; - match request { - PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkMessage { - account_id, - partial_encoded_chunk, - }, - ) => { - let partial_encoded_chunk = - PartialEncodedChunk::from(partial_encoded_chunk); - let message = - ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunk( - partial_encoded_chunk, - ); - self.shards_manager(&account_id).send(message); - } - PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkForward { account_id, forward }, - ) => { - let message = - ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkForward( - forward, - ); - self.shards_manager(&account_id).send(message); - } - _ => { - tracing::debug!(target: "test", ?request, "skipping unsupported request type"); - } - } - } - } - } - } - - /// Process all PartialEncodedChunkRequests in the network queue for a client - /// `id`: id for the client - pub fn process_partial_encoded_chunks_requests(&mut self, id: usize) { - while let Some(request) = self.network_adapters[id].pop() { - self.process_partial_encoded_chunk_request(id, request); - } - } - - /// Send the PartialEncodedChunkRequest to the target client, get response and process the response - pub fn process_partial_encoded_chunk_request( - &mut self, - id: usize, - request: PeerManagerMessageRequest, - ) { - if let PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkRequest { target, request, .. }, - ) = request - { - let target_id = self.account_to_client_index[&target.account_id.unwrap()]; - let response = self.get_partial_encoded_chunk_response(target_id, request); - if let Some(response) = response { - self.shards_manager_adapters[id].send( - ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkResponse { - partial_encoded_chunk_response: response, - received_time: Instant::now(), - }, - ); - } - } else { - panic!("The request is not a PartialEncodedChunk request {:?}", request); - } - } - - pub fn get_partial_encoded_chunk_response( - &mut self, - id: usize, - request: PartialEncodedChunkRequestMsg, - ) -> Option { - self.shards_manager_adapters[id].send( - ShardsManagerRequestFromNetwork::ProcessPartialEncodedChunkRequest { - partial_encoded_chunk_request: request.clone(), - route_back: CryptoHash::default(), - }, - ); - let response = self.network_adapters[id].pop_most_recent(); - match response { - Some(PeerManagerMessageRequest::NetworkRequests( - NetworkRequests::PartialEncodedChunkResponse { route_back: _, response }, - )) => return Some(response), - Some(response) => { - self.network_adapters[id].put_back_most_recent(response); - } - None => {} - } - - panic!( - "Failed to process PartialEncodedChunkRequest from shards manager {}: {:?}", - id, request - ); - } - - pub fn process_shards_manager_responses(&mut self, id: usize) -> bool { - let mut any_processed = false; - while let Some(msg) = self.client_adapters[id].pop() { - match msg { - ShardsManagerResponse::ChunkCompleted { partial_chunk, shard_chunk } => { - self.clients[id].on_chunk_completed( - partial_chunk, - shard_chunk, - Arc::new(|_| {}), - ); - } - ShardsManagerResponse::InvalidChunk(encoded_chunk) => { - self.clients[id].on_invalid_chunk(encoded_chunk); - } - ShardsManagerResponse::ChunkHeaderReadyForInclusion { - chunk_header, - chunk_producer, - } => { - self.clients[id] - .on_chunk_header_ready_for_inclusion(chunk_header, chunk_producer); - } - } - any_processed = true; - } - any_processed - } - - pub fn process_shards_manager_responses_and_finish_processing_blocks(&mut self, idx: usize) { - let _span = - tracing::debug_span!(target: "test", "process_shards_manager", client=idx).entered(); - - loop { - self.process_shards_manager_responses(idx); - if self.clients[idx].finish_blocks_in_processing().is_empty() { - return; - } - } - } - - pub fn send_money(&mut self, id: usize) -> ProcessTxResponse { - let account_id = self.get_client_id(0); - let signer = - InMemorySigner::from_seed(account_id.clone(), KeyType::ED25519, account_id.as_ref()); - let tx = SignedTransaction::send_money( - 1, - account_id.clone(), - account_id.clone(), - &signer, - 100, - self.clients[id].chain.head().unwrap().last_block_hash, - ); - self.clients[id].process_tx(tx, false, false) - } - - /// This function will actually bump to the latest protocol version instead of the provided one. - /// See https://github.com/near/nearcore/issues/8590 for details. - pub fn upgrade_protocol(&mut self, protocol_version: ProtocolVersion) { - assert_eq!(self.clients.len(), 1, "at the moment, this support only a single client"); - - let tip = self.clients[0].chain.head().unwrap(); - let epoch_id = self.clients[0] - .epoch_manager - .get_epoch_id_from_prev_block(&tip.last_block_hash) - .unwrap(); - let block_producer = - self.clients[0].epoch_manager.get_block_producer(&epoch_id, tip.height).unwrap(); - - let mut block = self.clients[0].produce_block(tip.height + 1).unwrap().unwrap(); - eprintln!("Producing block with version {protocol_version}"); - block.mut_header().set_latest_protocol_version(protocol_version); - block.mut_header().resign(&create_test_signer(block_producer.as_str())); - - let _ = self.clients[0] - .process_block_test_no_produce_chunk(block.into(), Provenance::NONE) - .unwrap(); - - for i in 0..self.clients[0].chain.epoch_length * 2 { - self.produce_block(0, tip.height + i + 2); - } - } - - pub fn query_account(&mut self, account_id: AccountId) -> AccountView { - let head = self.clients[0].chain.head().unwrap(); - let last_block = self.clients[0].chain.get_block(&head.last_block_hash).unwrap(); - let last_chunk_header = &last_block.chunks()[0]; - let response = self.clients[0] - .runtime_adapter - .query( - ShardUId::single_shard(), - &last_chunk_header.prev_state_root(), - last_block.header().height(), - last_block.header().raw_timestamp(), - last_block.header().prev_hash(), - last_block.header().hash(), - last_block.header().epoch_id(), - &QueryRequest::ViewAccount { account_id }, - ) - .unwrap(); - match response.kind { - QueryResponseKind::ViewAccount(account_view) => account_view, - _ => panic!("Wrong return value"), - } - } - - pub fn query_state(&mut self, account_id: AccountId) -> Vec { - let head = self.clients[0].chain.head().unwrap(); - let last_block = self.clients[0].chain.get_block(&head.last_block_hash).unwrap(); - let last_chunk_header = &last_block.chunks()[0]; - let response = self.clients[0] - .runtime_adapter - .query( - ShardUId::single_shard(), - &last_chunk_header.prev_state_root(), - last_block.header().height(), - last_block.header().raw_timestamp(), - last_block.header().prev_hash(), - last_block.header().hash(), - last_block.header().epoch_id(), - &QueryRequest::ViewState { - account_id, - prefix: vec![].into(), - include_proof: false, - }, - ) - .unwrap(); - match response.kind { - QueryResponseKind::ViewState(view_state_result) => view_state_result.values, - _ => panic!("Wrong return value"), - } - } - - pub fn query_balance(&mut self, account_id: AccountId) -> Balance { - self.query_account(account_id).amount - } - - /// Restarts client at given index. Note that the new client reuses runtime - /// adapter of old client. - /// TODO (#8269): create new `KeyValueRuntime` for new client. Currently it - /// doesn't work because `KeyValueRuntime` misses info about new epochs in - /// memory caches. - /// Though, it seems that it is not necessary for current use cases. - pub fn restart(&mut self, idx: usize) { - let account_id = self.get_client_id(idx).clone(); - let rng_seed = match self.seeds.get(&account_id) { - Some(seed) => *seed, - None => TEST_SEED, - }; - let vs = ValidatorSchedule::new().block_producers_per_epoch(vec![self.validators.clone()]); - let num_validator_seats = vs.all_block_producers().count() as NumSeats; - self.clients[idx] = setup_client_with_runtime( - num_validator_seats, - Some(self.get_client_id(idx).clone()), - false, - self.network_adapters[idx].clone().into(), - self.shards_manager_adapters[idx].clone(), - self.chain_genesis.clone(), - self.clients[idx].epoch_manager.clone(), - self.clients[idx].shard_tracker.clone(), - self.clients[idx].runtime_adapter.clone(), - rng_seed, - self.archive, - self.save_trie_changes, - None, - ) - } - - /// Returns an [`AccountId`] used by a client at given index. More - /// specifically, returns validator id of the client’s validator signer. - pub fn get_client_id(&self, idx: usize) -> &AccountId { - self.clients[idx].validator_signer.as_ref().unwrap().validator_id() - } - - pub fn get_runtime_config(&self, idx: usize, epoch_id: EpochId) -> RuntimeConfig { - self.clients[idx].runtime_adapter.get_protocol_config(&epoch_id).unwrap().runtime_config - } - - /// Create and sign transaction ready for execution. - pub fn tx_from_actions( - &mut self, - actions: Vec, - signer: &InMemorySigner, - receiver: AccountId, - ) -> SignedTransaction { - let tip = self.clients[0].chain.head().unwrap(); - SignedTransaction::from_actions( - tip.height + 1, - signer.account_id.clone(), - receiver, - signer, - actions, - tip.last_block_hash, - ) - } - - /// Wrap actions in a delegate action, put it in a transaction, sign. - pub fn meta_tx_from_actions( - &mut self, - actions: Vec, - sender: AccountId, - relayer: AccountId, - receiver_id: AccountId, - ) -> SignedTransaction { - let inner_signer = InMemorySigner::from_seed(sender.clone(), KeyType::ED25519, &sender); - let relayer_signer = InMemorySigner::from_seed(relayer.clone(), KeyType::ED25519, &relayer); - let tip = self.clients[0].chain.head().unwrap(); - let user_nonce = tip.height + 1; - let relayer_nonce = tip.height + 1; - let delegate_action = DelegateAction { - sender_id: inner_signer.account_id.clone(), - receiver_id, - actions: actions - .into_iter() - .map(|action| NonDelegateAction::try_from(action).unwrap()) - .collect(), - nonce: user_nonce, - max_block_height: tip.height + 100, - public_key: inner_signer.public_key(), - }; - let signature = inner_signer.sign(delegate_action.get_nep461_hash().as_bytes()); - let signed_delegate_action = SignedDelegateAction { delegate_action, signature }; - SignedTransaction::from_actions( - relayer_nonce, - relayer, - sender, - &relayer_signer, - vec![Action::Delegate(Box::new(signed_delegate_action))], - tip.last_block_hash, - ) - } - - /// Process a tx and its receipts, then return the execution outcome. - pub fn execute_tx( - &mut self, - tx: SignedTransaction, - ) -> Result { - let tx_hash = tx.get_hash(); - let response = self.clients[0].process_tx(tx, false, false); - // Check if the transaction got rejected - match response { - ProcessTxResponse::NoResponse - | ProcessTxResponse::RequestRouted - | ProcessTxResponse::ValidTx => (), - ProcessTxResponse::InvalidTx(e) => return Err(e), - ProcessTxResponse::DoesNotTrackShard => panic!("test setup is buggy"), - } - let max_iters = 100; - let tip = self.clients[0].chain.head().unwrap(); - for i in 0..max_iters { - let block = self.clients[0].produce_block(tip.height + i + 1).unwrap().unwrap(); - self.process_block(0, block.clone(), Provenance::PRODUCED); - if let Ok(outcome) = self.clients[0].chain.get_final_transaction_result(&tx_hash) { - // process one more block to allow refunds to finish - let block = self.clients[0].produce_block(tip.height + i + 2).unwrap().unwrap(); - self.process_block(0, block.clone(), Provenance::PRODUCED); - return Ok(outcome); - } - } - panic!("No transaction outcome found after {max_iters} blocks.") - } - - /// Execute a function call transaction that calls main on the `TestEnv`. - /// - /// This function assumes that account has been deployed and that - /// `InMemorySigner::from_seed` produces a valid signer that has it's key - /// deployed already. - pub fn call_main(&mut self, account: &AccountId) -> FinalExecutionOutcomeView { - let signer = InMemorySigner::from_seed(account.clone(), KeyType::ED25519, account.as_str()); - let actions = vec![Action::FunctionCall(Box::new(FunctionCallAction { - method_name: "main".to_string(), - args: vec![], - gas: 3 * 10u64.pow(14), - deposit: 0, - }))]; - let tx = self.tx_from_actions(actions, &signer, signer.account_id.clone()); - self.execute_tx(tx).unwrap() - } -} - -impl Drop for TestEnv { - fn drop(&mut self) { - let paused_blocks = self.paused_blocks.lock().unwrap(); - for cell in paused_blocks.values() { - let _ = cell.set(()); - } - if !paused_blocks.is_empty() && !std::thread::panicking() { - panic!("some blocks are still paused, did you call `resume_block_processing`?") - } - } -} - -pub fn create_chunk_on_height_for_shard( - client: &mut Client, - next_height: BlockHeight, - shard_id: ShardId, -) -> (EncodedShardChunk, Vec, Vec) { - let last_block_hash = client.chain.head().unwrap().last_block_hash; - let last_block = client.chain.get_block(&last_block_hash).unwrap(); - client - .produce_chunk( - last_block_hash, - &client.epoch_manager.get_epoch_id_from_prev_block(&last_block_hash).unwrap(), - Chain::get_prev_chunk_header(client.epoch_manager.as_ref(), &last_block, shard_id) - .unwrap(), - next_height, - shard_id, - ) - .unwrap() - .unwrap() -} - -pub fn create_chunk_on_height( - client: &mut Client, - next_height: BlockHeight, -) -> (EncodedShardChunk, Vec, Vec) { - create_chunk_on_height_for_shard(client, next_height, 0) -} - -pub fn create_chunk_with_transactions( - client: &mut Client, - transactions: Vec, -) -> (EncodedShardChunk, Vec, Vec, Block) { - create_chunk(client, Some(transactions), None) -} - -/// Create a chunk with specified transactions and possibly a new state root. -/// Useful for writing tests with challenges. -pub fn create_chunk( - client: &mut Client, - replace_transactions: Option>, - replace_tx_root: Option, -) -> (EncodedShardChunk, Vec, Vec, Block) { - let last_block = client.chain.get_block_by_height(client.chain.head().unwrap().height).unwrap(); - let next_height = last_block.header().height() + 1; - let (mut chunk, mut merkle_paths, receipts) = client - .produce_chunk( - *last_block.hash(), - last_block.header().epoch_id(), - last_block.chunks()[0].clone(), - next_height, - 0, - ) - .unwrap() - .unwrap(); - let should_replace = replace_transactions.is_some() || replace_tx_root.is_some(); - let transactions = replace_transactions.unwrap_or_else(Vec::new); - let tx_root = match replace_tx_root { - Some(root) => root, - None => merklize(&transactions).0, - }; - // reconstruct the chunk with changes (if any) - if should_replace { - // The best way it to decode chunk, replace transactions and then recreate encoded chunk. - let total_parts = client.chain.epoch_manager.num_total_parts(); - let data_parts = client.chain.epoch_manager.num_data_parts(); - let decoded_chunk = chunk.decode_chunk(data_parts).unwrap(); - let parity_parts = total_parts - data_parts; - let mut rs = ReedSolomonWrapper::new(data_parts, parity_parts); - - let signer = client.validator_signer.as_ref().unwrap().clone(); - let header = chunk.cloned_header(); - let (mut encoded_chunk, mut new_merkle_paths) = EncodedShardChunk::new( - *header.prev_block_hash(), - header.prev_state_root(), - header.prev_outcome_root(), - header.height_created(), - header.shard_id(), - &mut rs, - header.prev_gas_used(), - header.gas_limit(), - header.prev_balance_burnt(), - tx_root, - header.prev_validator_proposals().collect(), - transactions, - decoded_chunk.prev_outgoing_receipts(), - header.prev_outgoing_receipts_root(), - &*signer, - PROTOCOL_VERSION, - ) - .unwrap(); - swap(&mut chunk, &mut encoded_chunk); - swap(&mut merkle_paths, &mut new_merkle_paths); - } - match &mut chunk { - EncodedShardChunk::V1(chunk) => { - chunk.header.height_included = next_height; - } - EncodedShardChunk::V2(chunk) => { - *chunk.header.height_included_mut() = next_height; - } - } - let block_merkle_tree = client.chain.store().get_block_merkle_tree(last_block.hash()).unwrap(); - let mut block_merkle_tree = PartialMerkleTree::clone(&block_merkle_tree); - block_merkle_tree.insert(*last_block.hash()); - let block = Block::produce( - PROTOCOL_VERSION, - PROTOCOL_VERSION, - last_block.header(), - next_height, - last_block.header().block_ordinal() + 1, - vec![chunk.cloned_header()], - last_block.header().epoch_id().clone(), - last_block.header().next_epoch_id().clone(), - None, - vec![], - Ratio::new(0, 1), - 0, - 100, - None, - vec![], - vec![], - &*client.validator_signer.as_ref().unwrap().clone(), - *last_block.header().next_bp_hash(), - block_merkle_tree.root(), - None, - ); - (chunk, merkle_paths, receipts, block) -} - -/// Keep running catchup until there is no more catchup work that can be done -/// Note that this function does not necessarily mean that all blocks are caught up. -/// It's possible that some blocks that need to be caught up are still being processed -/// and the catchup process can't catch up on these blocks yet. -pub fn run_catchup( - client: &mut Client, - highest_height_peers: &[HighestHeightPeerInfo], -) -> Result<(), Error> { - let f = |_| {}; - let block_messages = Arc::new(RwLock::new(vec![])); - let block_inside_messages = block_messages.clone(); - let block_catch_up = move |msg: BlockCatchUpRequest| { - block_inside_messages.write().unwrap().push(msg); - }; - let state_split_messages = Arc::new(RwLock::new(vec![])); - let state_split_inside_messages = state_split_messages.clone(); - let state_split = move |msg: StateSplitRequest| { - state_split_inside_messages.write().unwrap().push(msg); - }; - let _ = System::new(); - let state_parts_arbiter_handle = Arbiter::new().handle(); - loop { - client.run_catchup( - highest_height_peers, - &f, - &block_catch_up, - &state_split, - Arc::new(|_| {}), - &state_parts_arbiter_handle, - )?; - let mut catchup_done = true; - for msg in block_messages.write().unwrap().drain(..) { - let results = do_apply_chunks(msg.block_hash, msg.block_height, msg.work); - if let Some((_, _, blocks_catch_up_state)) = - client.catchup_state_syncs.get_mut(&msg.sync_hash) - { - assert!(blocks_catch_up_state.scheduled_blocks.remove(&msg.block_hash)); - blocks_catch_up_state.processed_blocks.insert(msg.block_hash, results); - } else { - panic!("block catch up processing result from unknown sync hash"); - } - catchup_done = false; - } - for msg in state_split_messages.write().unwrap().drain(..) { - let response = Chain::build_state_for_split_shards(msg); - if let Some((sync, _, _)) = client.catchup_state_syncs.get_mut(&response.sync_hash) { - // We are doing catchup - sync.set_split_result(response.shard_id, response.new_state_roots); - } else { - client.state_sync.set_split_result(response.shard_id, response.new_state_roots); - } - catchup_done = false; - } - if catchup_done { - break; - } - } - Ok(()) -} diff --git a/chain/client/src/tests/mod.rs b/chain/client/src/tests/mod.rs index 3b45a4c05c7..aae48260312 100644 --- a/chain/client/src/tests/mod.rs +++ b/chain/client/src/tests/mod.rs @@ -7,3 +7,4 @@ mod doomslug; mod maintenance_windows; mod process_blocks; mod query_client; +mod test_utils; diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 2cee38f75ff..5d0d25ceff6 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -18,7 +18,7 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::{ Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, - FunctionCallAction, StakeAction, TransferAction, + FunctionCallAction, StakeAction }; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{ @@ -455,7 +455,6 @@ pub(crate) fn action_implicit_account_creation_transfer( account: &mut Option, actor_id: &mut AccountId, account_id: &AccountId, - transfer: &TransferAction, deposit: Balance, block_height: BlockHeight, current_protocol_version: ProtocolVersion, @@ -515,7 +514,7 @@ pub(crate) fn action_implicit_account_creation_transfer( + wallet_contract.code().len() as u64; *account = - Some(Account::new(transfer.deposit, 0, 0, *wallet_contract.hash(), storage_usage)); + Some(Account::new(deposit, 0, 0, *wallet_contract.hash(), storage_usage)); // TODO(eth-implicit) Store a reference to the `Wallet Contract` instead of literally deploying it. set_code(state_update, account_id.clone(), &wallet_contract); @@ -1056,6 +1055,8 @@ pub(crate) fn check_account_existence( Ok(()) } + +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] fn check_transfer_to_nonexisting_account( config: &RuntimeConfig, is_the_only_action: bool, diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 2288d750bb1..0909052ea9a 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -25,9 +25,9 @@ use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; use near_primitives::transaction::{ - Action, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, + Action, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, TransferAction, }; -use near_primitives::transaction::{ExecutionMetadata, TransferAction}; +use near_primitives::transaction::ExecutionMetadata; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas, @@ -307,7 +307,7 @@ impl Runtime { result.compute_usage = exec_fees; let account_id = &receipt.receiver_id; let is_the_only_action = actions.len() == 1; - let is_refund = AccountId::is_system(&receipt.predecessor_id); + let is_refund = receipt.predecessor_id.is_system(); let receipt_starts_with_create_account = matches!(actions.get(0), Some(Action::CreateAccount(_))); @@ -368,35 +368,20 @@ impl Runtime { epoch_info_provider, )?; } - Action::Transfer(transfer) => { - if let Some(account) = account.as_mut() { - action_transfer(account, transfer)?; - // Check if this is a gas refund, then try to refund the access key allowance. - if is_refund && action_receipt.signer_id == receipt.receiver_id { - try_refund_allowance( - state_update, - &receipt.receiver_id, - &action_receipt.signer_public_key, - transfer, - )?; - } - } else { - // Implicit account creation - debug_assert!(apply_state.config.wasm_config.implicit_account_creation); - debug_assert!(!is_refund); - action_implicit_account_creation_transfer( - state_update, - apply_state, - &apply_state.config.fees, - account, - actor_id, - &receipt.receiver_id, - transfer, - apply_state.block_height, - apply_state.current_protocol_version, - ); - } - }, + Action::Transfer(TransferAction { deposit }) => { + let nonrefundable = false; + action_transfer_or_implicit_account_creation( + account, + *deposit, + nonrefundable, + is_refund, + action_receipt, + receipt, + state_update, + apply_state, + actor_id, + )?; + } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] Action::TransferV2(transfer) => { action_transfer_or_implicit_account_creation( @@ -1561,6 +1546,7 @@ fn action_transfer_or_implicit_account_creation( debug_assert!(!is_refund); action_implicit_account_creation_transfer( state_update, + &apply_state, &apply_state.config.fees, account, actor_id, diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index 8e382e32df0..85e6dbf6342 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -750,7 +750,7 @@ impl ForkNetworkCommand { Account::new( liquid_balance, validator_account.amount, - 0 + 0, CryptoHash::default(), storage_bytes, ), From a52fdb4b8746329ca1a9efc3af70a2fce835a724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Mon, 18 Dec 2023 16:40:04 +0100 Subject: [PATCH 14/36] Fix compilation errors --- chain/rosetta-rpc/src/adapters/mod.rs | 18 ++++++++++++++---- core/primitives/src/action/mod.rs | 2 +- core/primitives/src/views.rs | 2 +- runtime/runtime/src/config.rs | 20 +++++++++++++------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 84032af28cf..8de2e345176 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -355,11 +355,9 @@ impl From for Vec { ); } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - near_primitives::transaction::Action::TransferV2( - near_primitives::transaction::TransferActionV2 { deposit, .. }, - ) => { + near_primitives::transaction::Action::TransferV2(action) => { // TODO(protocol_feature_nonrefundable_transfer_nep491): merge with branch above on stabilization - let transfer_amount = crate::models::Amount::from_yoctonear(deposit); + let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); let sender_transfer_operation_id = crate::models::OperationIdentifier::new(&operations); @@ -893,6 +891,8 @@ mod tests { amount: 5000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -908,6 +908,8 @@ mod tests { amount: 4000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -921,6 +923,8 @@ mod tests { amount: 7000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -936,6 +940,8 @@ mod tests { amount: 8000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -949,6 +955,8 @@ mod tests { amount: 4000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -959,6 +967,8 @@ mod tests { amount: 6000000000000000000, code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, storage_paid_at: 0, storage_usage: 200000, }, diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index 593bb6cd99b..ad35c77565d 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -197,7 +197,7 @@ pub enum Action { DeleteAccount(DeleteAccountAction), Delegate(Box), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - TransferV2(TransferActionV2), + TransferV2(Box), } const _: () = assert!( cfg!(not(target_pointer_width = "64")) || std::mem::size_of::() == 32, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 5a5fecfaac6..b87b17403f8 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1290,7 +1290,7 @@ impl TryFrom for Action { // Is this good enough? Must the Action -> View -> Action conversion be lossless? #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] ActionView::Transfer { deposit, nonrefundable } => { - Action::TransferV2(crate::transaction::TransferActionV2 { deposit, nonrefundable }) + Action::TransferV2(Box::new(crate::transaction::TransferActionV2 { deposit, nonrefundable })) } ActionView::Stake { stake, public_key } => { Action::Stake(Box::new(StakeAction { stake, public_key })) diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index ebd6f578e47..37250859c6e 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -108,9 +108,13 @@ pub fn total_send_fees( TransferV2(_) => { // Note: when stabilizing, merge with branch above // Account for implicit account creation - let is_receiver_implicit = - config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); - transfer_send_fee(fees, sender_is_receiver, is_receiver_implicit) + transfer_send_fee( + fees, + sender_is_receiver, + config.wasm_config.implicit_account_creation, + config.wasm_config.eth_implicit_accounts, + receiver_id.get_account_type(), + ) } Stake(_) => fees.fee(ActionCosts::stake).send_fee(sender_is_receiver), AddKey(add_key_action) => match &add_key_action.access_key.permission { @@ -210,11 +214,13 @@ pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] TransferV2(_) => { // Note: when stabilizing, merge with branch above - // Account for implicit account creation - let is_receiver_implicit = - config.wasm_config.implicit_account_creation && receiver_id.is_implicit(); - transfer_exec_fee(fees, is_receiver_implicit) + transfer_exec_fee( + fees, + config.wasm_config.implicit_account_creation, + config.wasm_config.eth_implicit_accounts, + receiver_id.get_account_type(), + ) } Stake(_) => fees.fee(ActionCosts::stake).exec_fee(), AddKey(add_key_action) => match &add_key_action.access_key.permission { From ae36baef74a45844a4d9246b5f0175b5d31949d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 19 Dec 2023 11:16:19 +0100 Subject: [PATCH 15/36] Build fixes --- Cargo.lock | 8 +-- chain/client/src/tests/mod.rs | 1 - chain/jsonrpc/res/rpc_errors_schema.json | 10 ++- core/primitives-core/src/account.rs | 2 +- core/primitives/src/views.rs | 5 +- .../tests/client/features/chunk_validation.rs | 2 +- .../tests/client/features/in_memory_tries.rs | 2 +- .../client/features/nonrefundable_transfer.rs | 4 +- neard/Cargo.toml | 3 +- runtime/runtime/src/actions.rs | 12 ++-- runtime/runtime/src/balance_checker.rs | 67 ++++++++++++++++++- runtime/runtime/src/lib.rs | 4 +- .../runtime/tests/runtime_group_tools/mod.rs | 8 ++- tools/amend-genesis/src/lib.rs | 9 ++- tools/state-viewer/Cargo.toml | 5 +- 15 files changed, 112 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa7076f3e53..ee9cd849a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8588,18 +8588,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", diff --git a/chain/client/src/tests/mod.rs b/chain/client/src/tests/mod.rs index aae48260312..3b45a4c05c7 100644 --- a/chain/client/src/tests/mod.rs +++ b/chain/client/src/tests/mod.rs @@ -7,4 +7,3 @@ mod doomslug; mod maintenance_windows; mod process_blocks; mod query_client; -mod test_utils; diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json index 1279c61751e..85a209dc090 100644 --- a/chain/jsonrpc/res/rpc_errors_schema.json +++ b/chain/jsonrpc/res/rpc_errors_schema.json @@ -46,7 +46,8 @@ "DelegateActionExpired", "DelegateActionAccessKeyError", "DelegateActionInvalidNonce", - "DelegateActionNonceTooLarge" + "DelegateActionNonceTooLarge", + "NonRefundableBalanceToExistingAccount" ], "props": { "index": "" @@ -640,6 +641,13 @@ "subtypes": [], "props": {} }, + "NonRefundableBalanceToExistingAccount": { + "name": "NonRefundableBalanceToExistingAccount", + "subtypes": [], + "props": { + "account_id": "" + } + }, "NonceTooLarge": { "name": "NonceTooLarge", "subtypes": [], diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 07af8e5ea96..9d601a8defe 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -372,7 +372,7 @@ mod tests { #[test] fn test_account_serialization() { let acc = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); - let bytes = acc.try_to_vec().unwrap(); + let bytes = borsh::to_vec(&acc).unwrap(); if cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491") { expect_test::expect!("HaZPNG4KpXQ9Mre4PAA83V5usqXsA4zy4vMwSXBiBcQv") } else { diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index aa7f69be362..39cd7c3e2ce 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1294,7 +1294,10 @@ impl TryFrom for Action { // Is this good enough? Must the Action -> View -> Action conversion be lossless? #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] ActionView::Transfer { deposit, nonrefundable } => { - Action::TransferV2(Box::new(crate::transaction::TransferActionV2 { deposit, nonrefundable })) + Action::TransferV2(Box::new(crate::transaction::TransferActionV2 { + deposit, + nonrefundable, + })) } ActionView::Stake { stake, public_key } => { Action::Stake(Box::new(StakeAction { stake, public_key })) diff --git a/integration-tests/src/tests/client/features/chunk_validation.rs b/integration-tests/src/tests/client/features/chunk_validation.rs index 152dc8c131b..a0a9478ae0c 100644 --- a/integration-tests/src/tests/client/features/chunk_validation.rs +++ b/integration-tests/src/tests/client/features/chunk_validation.rs @@ -70,7 +70,7 @@ fn test_chunk_validation_basic() { let staked = if i < 8 { validator_stake } else { 0 }; records.push(StateRecord::Account { account_id: account.clone(), - account: Account::new(0, staked, CryptoHash::default(), 0), + account: Account::new(0, staked, 0, CryptoHash::default(), 0), }); // The total supply must be correct to pass validation. genesis_config.total_supply += staked; diff --git a/integration-tests/src/tests/client/features/in_memory_tries.rs b/integration-tests/src/tests/client/features/in_memory_tries.rs index fe258758ae6..71dd5741d7d 100644 --- a/integration-tests/src/tests/client/features/in_memory_tries.rs +++ b/integration-tests/src/tests/client/features/in_memory_tries.rs @@ -102,7 +102,7 @@ fn test_in_memory_trie_node_consistency() { let staked = if i < 2 { validator_stake } else { 0 }; records.push(StateRecord::Account { account_id: account.clone(), - account: Account::new(initial_balance, staked, CryptoHash::default(), 0), + account: Account::new(initial_balance, staked, 0, CryptoHash::default(), 0), }); records.push(StateRecord::AccessKey { account_id: account.clone(), diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index f02ca6070f7..6fb5f79b85a 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -6,7 +6,6 @@ //! //! NEP: https://github.com/near/NEPs/pull/491 -use crate::tests::client::utils::TestEnvNightshadeSetupExt; use near_chain::ChainGenesis; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; @@ -17,6 +16,7 @@ use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives::views::FinalExecutionOutcomeView; use nearcore::config::GenesisExt; +use nearcore::test_utils::TestEnvNightshadeSetupExt; /// Refundable transfer V2 successfully adds balance like a transfer V1. #[test] @@ -91,7 +91,7 @@ fn exec_transfer_v2( let sender_pre_balance = env.query_balance(sender()); let receiver_before = env.query_account(receiver()); - let transfer = Action::TransferV2(TransferActionV2 { deposit, nonrefundable }); + let transfer = Action::TransferV2(Box::new(TransferActionV2 { deposit, nonrefundable })); let tx = env.tx_from_actions(vec![transfer], &signer, receiver()); let status = env.execute_tx(tx); diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 9050c6808c8..33d7b7de6c0 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -74,12 +74,14 @@ no_cache = ["nearcore/no_cache"] rosetta_rpc = ["nearcore/rosetta_rpc"] json_rpc = ["nearcore/json_rpc"] protocol_feature_fix_staking_threshold = ["nearcore/protocol_feature_fix_staking_threshold"] +protocol_feature_nonrefundable_transfer_nep491 = ["near-state-viewer/protocol_feature_nonrefundable_transfer_nep491"] serialize_all_state_changes = ["nearcore/serialize_all_state_changes"] new_epoch_sync = ["nearcore/new_epoch_sync", "dep:near-epoch-sync-tool"] nightly = [ "nightly_protocol", "protocol_feature_fix_staking_threshold", + "protocol_feature_nonrefundable_transfer_nep491", "serialize_all_state_changes", "near-chain-configs/nightly", "near-client/nightly", @@ -94,7 +96,6 @@ nightly = [ "near-primitives/nightly", "near-state-parts-dump-check/nightly", "near-state-parts/nightly", - "near-state-viewer/nightly", "near-store/nightly", "near-undo-block/nightly", "nearcore/nightly", diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index f14cf46a84a..e99a3e0f6cf 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -18,15 +18,13 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::{ Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, - FunctionCallAction, StakeAction + FunctionCallAction, StakeAction, }; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{ AccountId, Balance, BlockHeight, EpochInfoProvider, Gas, TrieCacheMode, }; -use near_primitives::utils::{ - account_is_implicit, create_random_seed, wallet_contract_placeholder, -}; +use near_primitives::utils::{account_is_implicit, create_random_seed}; use near_primitives::version::{ ProtocolFeature, ProtocolVersion, DELETE_KEY_STORAGE_USAGE_PROTOCOL_VERSION, }; @@ -539,8 +537,7 @@ pub(crate) fn action_implicit_account_creation_transfer( + magic_bytes.code().len() as u64 + fee_config.storage_usage_config.num_extra_bytes_record; - *account = - Some(Account::new(transfer.deposit, 0, 0, *magic_bytes.hash(), storage_usage)); + *account = Some(Account::new(deposit, 0, 0, *magic_bytes.hash(), storage_usage)); set_code(state_update, account_id.clone(), &magic_bytes); // Precompile Wallet Contract and store result (compiled code or error) in the database. @@ -1080,7 +1077,6 @@ pub(crate) fn check_account_existence( Ok(()) } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] fn check_transfer_to_nonexisting_account( config: &RuntimeConfig, @@ -1090,7 +1086,7 @@ fn check_transfer_to_nonexisting_account( ) -> Result<(), ActionError> { if config.wasm_config.implicit_account_creation && is_the_only_action - && account_is_implicit(account_id, config.wasm_config.eth_implicit_accounts) + && account_is_implicit(account_id, config.wasm_config.eth_implicit_accounts) && !is_refund { // OK. It's implicit account creation. diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 6cf16b1dfef..55173565deb 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -427,6 +427,7 @@ mod tests { .unwrap(); } + /// This tests shows how overflow (which we do not expect) would be handled on a transfer. #[test] fn test_total_balance_overflow_returns_unexpected_overflow() { let tries = TestTriesBuilder::new().build(); @@ -437,8 +438,9 @@ mod tests { let deposit = 1000; let mut initial_state = tries.new_trie_update(ShardUId::single_shard(), root); - let alice = account_new(u128::MAX, hash(&[])); - let bob = account_new(1u128, hash(&[])); + // u128::MAX is used as a sentinel value for account version 2 or higher, see https://github.com/near/NEPs/pull/491. + let alice = account_new(u128::MAX - 1, hash(&[])); + let bob = account_new(2u128, hash(&[])); set_account(&mut initial_state, alice_id.clone(), &alice); set_account(&mut initial_state, bob_id.clone(), &bob); @@ -447,8 +449,9 @@ mod tests { let signer = InMemorySigner::from_seed(alice_id.clone(), KeyType::ED25519, alice_id.as_ref()); + // Sending 2, so that we have an overflow when adding to alice's balance. let tx = - SignedTransaction::send_money(0, alice_id, bob_id, &signer, 1, CryptoHash::default()); + SignedTransaction::send_money(0, alice_id, bob_id, &signer, 2, CryptoHash::default()); let receipt = Receipt { predecessor_id: tx.transaction.signer_id.clone(), @@ -477,4 +480,62 @@ mod tests { Err(RuntimeError::UnexpectedIntegerOverflow) ); } + + /// This tests shows what would happen if the total balance becomes u128::MAX, + /// a sentinel value use to distinguish between accounts version 1 and 2+, + /// see https://github.com/near/NEPs/pull/491. + #[test] + fn test_total_balance_u128_max() { + let tries = TestTriesBuilder::new().build(); + let root = MerkleHash::default(); + let alice_id = alice_account(); + let bob_id = bob_account(); + let gas_price = 100; + let deposit = 1000; + + let mut initial_state = tries.new_trie_update(ShardUId::single_shard(), root); + let alice = account_new(u128::MAX - 1, hash(&[])); + let bob = account_new(1u128, hash(&[])); + + set_account(&mut initial_state, alice_id.clone(), &alice); + set_account(&mut initial_state, bob_id.clone(), &bob); + initial_state.commit(StateChangeCause::NotWritableToDisk); + + let signer = + InMemorySigner::from_seed(alice_id.clone(), KeyType::ED25519, alice_id.as_ref()); + + let tx = + SignedTransaction::send_money(0, alice_id, bob_id, &signer, 1, CryptoHash::default()); + + let receipt = Receipt { + predecessor_id: tx.transaction.signer_id.clone(), + receiver_id: tx.transaction.receiver_id.clone(), + receipt_id: Default::default(), + receipt: ReceiptEnum::Action(ActionReceipt { + signer_id: tx.transaction.signer_id.clone(), + signer_public_key: tx.transaction.public_key.clone(), + gas_price, + output_data_receivers: vec![], + input_data_ids: vec![], + actions: vec![Action::Transfer(TransferAction { deposit })], + }), + }; + + // Alice's balance becomes u128::MAX, which causes it is interpreted as + // the Alice's account version to be 2 or higher, instead of being interpreted + // as Alice's balance. Another field is then interpreted as the balance which causes + // `BalanceMismatchError`. + assert_matches!( + check_balance( + &RuntimeConfig::test(), + &initial_state, + &None, + &[receipt], + &[tx], + &[], + &ApplyStats::default(), + ), + Err(RuntimeError::BalanceMismatchError { .. }) + ); + } } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 0909052ea9a..0eaaaa7accb 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -25,9 +25,9 @@ use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; use near_primitives::transaction::{ - Action, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, TransferAction, + Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, + SignedTransaction, TransferAction, }; -use near_primitives::transaction::ExecutionMetadata; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas, diff --git a/runtime/runtime/tests/runtime_group_tools/mod.rs b/runtime/runtime/tests/runtime_group_tools/mod.rs index 0b5166f089c..a10d20ed32e 100644 --- a/runtime/runtime/tests/runtime_group_tools/mod.rs +++ b/runtime/runtime/tests/runtime_group_tools/mod.rs @@ -220,7 +220,13 @@ impl RuntimeGroup { if (i as u64) < num_existing_accounts { state_records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(TESTING_INIT_BALANCE, TESTING_INIT_STAKE, 0, code_hash, 0), + account: Account::new( + TESTING_INIT_BALANCE, + TESTING_INIT_STAKE, + 0, + code_hash, + 0, + ), }); state_records.push(StateRecord::AccessKey { account_id: account_id.clone(), diff --git a/tools/amend-genesis/src/lib.rs b/tools/amend-genesis/src/lib.rs index edecbf1ed17..57981bdebd3 100644 --- a/tools/amend-genesis/src/lib.rs +++ b/tools/amend-genesis/src/lib.rs @@ -459,8 +459,13 @@ mod test { Self::Account { account_id, amount, locked, storage_usage } => { // `nonrefundable_balance` can be implemented if this is required in state records. let nonrefundable_balance = 0; - let account = - Account::new(*amount, *locked, nonrefundable_balance, CryptoHash::default(), *storage_usage); + let account = Account::new( + *amount, + *locked, + nonrefundable_balance, + CryptoHash::default(), + *storage_usage, + ); StateRecord::Account { account_id: account_id.parse().unwrap(), account } } Self::AccessKey { account_id, public_key } => StateRecord::AccessKey { diff --git a/tools/state-viewer/Cargo.toml b/tools/state-viewer/Cargo.toml index a0a8c17b872..377eeaea6eb 100644 --- a/tools/state-viewer/Cargo.toml +++ b/tools/state-viewer/Cargo.toml @@ -56,7 +56,10 @@ insta.workspace = true [features] sandbox = ["node-runtime/sandbox", "near-chain/sandbox", "near-client/sandbox"] -protocol_feature_nonrefundable_transfer_nep491 = [] +protocol_feature_nonrefundable_transfer_nep491 = [ + "near-primitives/protocol_feature_nonrefundable_transfer_nep491", +] + nightly = [ "nightly_protocol", "protocol_feature_nonrefundable_transfer_nep491", From ac4b238849fbf73b11a4d6658656a9ce8fa61be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 19 Dec 2023 19:53:19 +0100 Subject: [PATCH 16/36] Parse genesis config as it have nonrefundable fields set to 0. --- chain/chain/src/tests/simple_chain.rs | 4 ++-- core/chain-configs/src/genesis_validate.rs | 2 +- core/primitives-core/src/account.rs | 9 +++++++-- nearcore/Cargo.toml | 4 ++++ nearcore/src/runtime/tests.rs | 9 ++++++++- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index 9c68c295f03..1288d7e0910 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -52,7 +52,7 @@ fn build_chain() { // cargo insta test --accept -p near-chain --features nightly -- tests::simple_chain::build_chain let hash = chain.head().unwrap().last_block_hash; if cfg!(feature = "nightly") { - insta::assert_display_snapshot!(hash, @"CwaiZ4AmfJSnMN9rytYwwYHCTzLioC5xcjHzNkDex1HH"); + insta::assert_display_snapshot!(hash, @"8WF4fG7WCM2ysZvFQAgEfTEfBovtULxnWeRpwAt3BTBJ"); } else { insta::assert_display_snapshot!(hash, @"HJmRPXT4JM9tt6mXw2gM75YaSoqeDCphhFK26uRpd1vw"); } @@ -82,7 +82,7 @@ fn build_chain() { let hash = chain.head().unwrap().last_block_hash; if cfg!(feature = "nightly") { - insta::assert_display_snapshot!(hash, @"Dn18HUFm149fojXpwV1dYCfjdPh56S1k233kp7vmnFeE"); + insta::assert_display_snapshot!(hash, @"3iXi6BshQaPx9TsbDt5itAXUjnTQz9AR9pg2w349TFNj"); } else { insta::assert_display_snapshot!(hash, @"HbQVGVZ3WGxsNqeM3GfSwDoxwYZ2RBP1SinAze9SYR3C"); } diff --git a/core/chain-configs/src/genesis_validate.rs b/core/chain-configs/src/genesis_validate.rs index a529d22c4df..39535eca7a8 100644 --- a/core/chain-configs/src/genesis_validate.rs +++ b/core/chain-configs/src/genesis_validate.rs @@ -58,7 +58,7 @@ impl<'a> GenesisValidator<'a> { format!("Duplicate account id {} in genesis records", account_id); self.validation_errors.push_genesis_semantics_error(error_message) } - self.total_supply += account.locked() + account.amount(); + self.total_supply += account.locked() + account.amount() + account.nonrefundable(); self.account_ids.insert(account_id.clone()); if account.locked() > 0 { self.staked_accounts.insert(account_id.clone(), account.locked()); diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 9d601a8defe..06b02644208 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -48,8 +48,9 @@ pub struct Account { #[serde(with = "dec_format")] locked: Balance, /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. - #[serde(with = "dec_format")] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + #[serde(default = "Account::default_nonrefundable")] + #[serde(with = "dec_format")] nonrefundable: Balance, /// Hash of the code stored in the storage for this account. code_hash: CryptoHash, @@ -104,10 +105,14 @@ impl Account { self.nonrefundable } + fn default_nonrefundable() -> Balance { + 0 + } + #[inline] #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] pub fn nonrefundable(&self) -> Balance { - 0 + Self::default_nonrefundable() } #[inline] diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 06d0e13d6b2..cfef60a47d7 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -115,6 +115,9 @@ protocol_feature_fix_staking_threshold = [ protocol_feature_fix_contract_loading_cost = [ "near-vm-runner/protocol_feature_fix_contract_loading_cost", ] +protocol_feature_nonrefundable_transfer_nep491 = [ + "near-primitives/protocol_feature_nonrefundable_transfer_nep491", +] new_epoch_sync = [ "near-client/new_epoch_sync" ] @@ -124,6 +127,7 @@ nightly = [ "nightly_protocol", "protocol_feature_fix_contract_loading_cost", "protocol_feature_fix_staking_threshold", + "protocol_feature_nonrefundable_transfer_nep491", "serialize_all_state_changes", "near-async/nightly", "near-chain-configs/nightly", diff --git a/nearcore/src/runtime/tests.rs b/nearcore/src/runtime/tests.rs index e426d997aca..2f0e1fda302 100644 --- a/nearcore/src/runtime/tests.rs +++ b/nearcore/src/runtime/tests.rs @@ -1434,7 +1434,14 @@ fn test_genesis_hash() { let block = Chain::make_genesis_block(epoch_manager.as_ref(), runtime.as_ref(), &chain_genesis) .unwrap(); - assert_eq!(block.header().hash().to_string(), "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"); + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let block_expected_hash = "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let block_expected_hash = "3Cm2e8aZQuZbbnwWMNES6WnuszR7r2zzhdDhBK1Ft3f1"; + + assert_eq!(block.header().hash().to_string(), block_expected_hash); let epoch_manager = EpochManager::new_from_genesis_config(store, &genesis.config).unwrap(); let epoch_info = epoch_manager.get_epoch_info(&EpochId::default()).unwrap(); From f9248d3a498f7504cc9dd00f70a09b1d7623f7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Thu, 21 Dec 2023 15:52:11 +0100 Subject: [PATCH 17/36] Fix nonrefundable_transfer test --- .../src/tests/client/features/nonrefundable_transfer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 6fb5f79b85a..0391e777ed9 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -95,6 +95,10 @@ fn exec_transfer_v2( let tx = env.tx_from_actions(vec![transfer], &signer, receiver()); let status = env.execute_tx(tx); + let height = env.clients[0].chain.head().unwrap().height; + for i in 0..2 { + env.produce_block(0, height + 1 + i); + } if let Ok(outcome) = &status { let gas_cost = outcome.gas_cost(); From 5773e44221e0c229e09b8fef7833c6476d2206de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 2 Jan 2024 11:28:46 +0100 Subject: [PATCH 18/36] Add missing Non-refundable transfer tests --- chain/client/src/test_utils/test_env.rs | 20 +- core/primitives/src/test_utils.rs | 2 +- .../client/features/nonrefundable_transfer.rs | 367 ++++++++++++++---- .../tests/client/features/wallet_contract.rs | 27 +- 4 files changed, 311 insertions(+), 105 deletions(-) diff --git a/chain/client/src/test_utils/test_env.rs b/chain/client/src/test_utils/test_env.rs index 08a91a7b53f..9af3d0bfc77 100644 --- a/chain/client/src/test_utils/test_env.rs +++ b/chain/client/src/test_utils/test_env.rs @@ -7,6 +7,7 @@ use crate::Client; use near_async::messaging::CanSend; use near_chain::test_utils::ValidatorSchedule; use near_chain::{ChainGenesis, Provenance}; +use near_chain_primitives::error::QueryError; use near_chunks::client::ShardsManagerResponse; use near_chunks::test_utils::MockClientAdapterForShardsManager; use near_crypto::{InMemorySigner, KeyType, Signer}; @@ -30,8 +31,10 @@ use near_primitives::types::{AccountId, Balance, BlockHeight, EpochId, NumSeats} use near_primitives::utils::MaybeValidated; use near_primitives::version::ProtocolVersion; use near_primitives::views::{ - AccountView, FinalExecutionOutcomeView, QueryRequest, QueryResponseKind, StateItem, + AccountView, FinalExecutionOutcomeView, QueryRequest, QueryResponse, QueryResponseKind, + StateItem, }; +use near_store::ShardUId; use once_cell::sync::OnceCell; use super::setup::{setup_client_with_runtime, ShardsManagerAdapterForTest}; @@ -377,6 +380,21 @@ impl TestEnv { } } + pub fn query_view(&mut self, request: QueryRequest) -> Result { + let head = self.clients[0].chain.head().unwrap(); + let head_block = self.clients[0].chain.get_block(&head.last_block_hash).unwrap(); + self.clients[0].runtime_adapter.query( + ShardUId::single_shard(), + &head_block.chunks()[0].prev_state_root(), + head.height, + 0, + &head.prev_block_hash, + &head.last_block_hash, + head_block.header().epoch_id(), + &request, + ) + } + pub fn query_state(&mut self, account_id: AccountId) -> Vec { let client = &self.clients[0]; let head = client.chain.head().unwrap(); diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index a0fac74021f..34ef47d4248 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -570,7 +570,7 @@ impl FinalExecutionOutcomeView { #[track_caller] /// Check transaction and all transitive receipts for success status. pub fn assert_success(&self) { - assert!(matches!(self.status, FinalExecutionStatus::SuccessValue(_))); + assert!(matches!(self.status, FinalExecutionStatus::SuccessValue(_)), "{:?}", self.status); for (i, receipt) in self.receipts_outcome.iter().enumerate() { assert!( matches!( diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 0391e777ed9..7c417353f2c 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -9,112 +9,321 @@ use near_chain::ChainGenesis; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; -use near_crypto::{InMemorySigner, KeyType}; -use near_primitives::errors::{ActionsValidationError, InvalidTxError}; -use near_primitives::transaction::{Action, TransferActionV2}; +use near_crypto::{InMemorySigner, KeyType, PublicKey}; +use near_primitives::errors::{ + ActionError, ActionErrorKind, ActionsValidationError, InvalidTxError, TxExecutionError, +}; +use near_primitives::test_utils::near_implicit_test_account; +use near_primitives::transaction::{ + Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, + SignedTransaction, TransferActionV2, +}; use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; -use near_primitives::views::FinalExecutionOutcomeView; +use near_primitives::views::{ + ExecutionStatusView, FinalExecutionOutcomeView, QueryRequest, QueryResponseKind, +}; +use near_primitives_core::account::{AccessKey, AccessKeyPermission}; use nearcore::config::GenesisExt; use nearcore::test_utils::TestEnvNightshadeSetupExt; +use nearcore::NEAR_BASE; -/// Refundable transfer V2 successfully adds balance like a transfer V1. -#[test] -fn transfer_v2() { - let protocol_version = ProtocolFeature::NonRefundableBalance.protocol_version(); - exec_transfer_v2(protocol_version, 1, false) - .expect("Transfer V2 should be accepted") - .assert_success(); +// Default sender to use in tests of this module. +fn sender() -> AccountId { + "test0".parse().unwrap() } -// TODO: Test non-refundable transfer is rejected on existing account -// TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating implicit account -// TODO: Test for non-refundable transfer V2 successfully adding non-refundable balance when creating named account -// TODO: Test non-refundable balance allowing to have account with zero balance and more than 1kB of state -// TODO: Test non-refundable balance cannot be transferred -// TODO: Test for deleting an account with non-refundable storage (might rip up the balance checker) +/// Default receiver to use in tests of this module. +fn receiver() -> AccountId { + "test1".parse().unwrap() +} -/// During the protocol upgrade phase, before the voting completes, we must not -/// include transfer V2 actions on the chain. -/// -/// The correct way to handle it is to reject transaction before they even get -/// into the transaction pool. Hence, we check that an `InvalidTxError` error is -/// returned for older protocol versions. -#[test] -fn reject_transfer_v2_in_older_versions() { - let protocol_version = ProtocolFeature::NonRefundableBalance.protocol_version() - 1; - - let status = exec_transfer_v2(protocol_version, 1, false); - assert!( - matches!( - &status, - Err( - InvalidTxError::ActionsValidation( - ActionsValidationError::UnsupportedProtocolFeature{ protocol_feature, version } - ) - ) - if protocol_feature == "NonRefundableBalance" && *version == ProtocolFeature::NonRefundableBalance.protocol_version() - ), - "{status:?}", - ); +/// Default signer (corresponding to the default sender) to use in tests of this module. +fn signer() -> InMemorySigner { + InMemorySigner::from_seed(sender(), KeyType::ED25519, "test0") } -/// Sender implicitly used in all test of this module. -fn sender() -> AccountId { - "test0".parse().unwrap() +/// Creates a test environment using given protocol version (if some). +fn setup_env_with_protocol_version(protocol_version: Option) -> TestEnv { + let mut genesis = Genesis::test(vec![sender(), receiver()], 1); + if let Some(protocol_version) = protocol_version { + genesis.config.protocol_version = protocol_version; + } + TestEnv::builder(ChainGenesis::test()) + .real_epoch_managers(&genesis.config) + .nightshade_runtimes(&genesis) + .build() } -/// Receiver implicitly used in all test of this module. -fn receiver() -> AccountId { - "test1".parse().unwrap() +/// Creates a test environment without modifying protocol version. +fn setup_env() -> TestEnv { + setup_env_with_protocol_version(None) +} + +fn get_nonce(env: &mut TestEnv, signer: &InMemorySigner) -> u64 { + let request = QueryRequest::ViewAccessKey { + account_id: signer.account_id.clone(), + public_key: signer.public_key.clone(), + }; + match env.query_view(request).unwrap().kind { + QueryResponseKind::AccessKey(view) => view.nonce, + _ => panic!("wrong query response"), + } +} + +fn account_exists(env: &mut TestEnv, account_id: AccountId) -> bool { + let request = QueryRequest::ViewAccount { account_id }; + env.query_view(request).is_ok() +} + +fn execute_transaction_from_actions( + env: &mut TestEnv, + actions: Vec, + signer: &InMemorySigner, + receiver: AccountId, +) -> Result { + let tip = env.clients[0].chain.head().unwrap(); + let nonce = get_nonce(env, signer); + let tx = SignedTransaction::from_actions( + nonce + 1, + signer.account_id.clone(), + receiver, + signer, + actions, + tip.last_block_hash, + ); + let tx_result = env.execute_tx(tx); + let height = env.clients[0].chain.head().unwrap().height; + for i in 0..2 { + env.produce_block(0, height + 1 + i); + } + tx_result } -/// Creates a test environment and submits a transfer V2 action. +/// Submits a transfer V2 action. /// /// This methods checks that the balance is subtracted from the sender and added /// to the receiver, if the status was ok. No checks are done on an error. fn exec_transfer_v2( - protocol_version: ProtocolVersion, + env: &mut TestEnv, + signer: InMemorySigner, + receiver: AccountId, deposit: Balance, nonrefundable: bool, + account_creation: bool, + implicit_account_creation: bool, + deploy_contract: bool, ) -> Result { - let mut genesis = Genesis::test(vec![sender(), receiver()], 1); - let signer = InMemorySigner::from_seed(sender(), KeyType::ED25519, "test0"); - - genesis.config.protocol_version = protocol_version; + let sender_pre_balance = env.query_balance(sender()); + let (receiver_before_amount, receiver_before_nonrefundable) = if account_creation { + (0, 0) + } else { + let receiver_before = env.query_account(receiver.clone()); + (receiver_before.amount, receiver_before.nonrefundable) + }; - let mut env = TestEnv::builder(ChainGenesis::test()) - .real_epoch_managers(&genesis.config) - .nightshade_runtimes(&genesis) - .build(); + let mut actions = vec![]; + if account_creation && !implicit_account_creation { + actions.push(Action::CreateAccount(CreateAccountAction {})); + actions.push(Action::AddKey(Box::new(AddKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, receiver.as_str()), + access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, + }))); + } + actions.push(Action::TransferV2(Box::new(TransferActionV2 { deposit, nonrefundable }))); + if deploy_contract { + let contract = near_test_contracts::sized_contract(1500 as usize); + actions.push(Action::DeployContract(DeployContractAction { code: contract.to_vec() })) + } - let sender_pre_balance = env.query_balance(sender()); - let receiver_before = env.query_account(receiver()); + let tx_result = execute_transaction_from_actions(env, actions, &signer, receiver.clone()); - let transfer = Action::TransferV2(Box::new(TransferActionV2 { deposit, nonrefundable })); - let tx = env.tx_from_actions(vec![transfer], &signer, receiver()); + let outcome = match &tx_result { + Ok(outcome) => outcome, + _ => { + return tx_result; + } + }; - let status = env.execute_tx(tx); - let height = env.clients[0].chain.head().unwrap().height; - for i in 0..2 { - env.produce_block(0, height + 1 + i); + if !matches!(outcome.status, near_primitives::views::FinalExecutionStatus::SuccessValue(_)) { + return tx_result; } - if let Ok(outcome) = &status { - let gas_cost = outcome.gas_cost(); - assert_eq!(sender_pre_balance - deposit - gas_cost, env.query_balance(sender())); - - if matches!(outcome.status, near_primitives::views::FinalExecutionStatus::SuccessValue(_)) { - let receiver_after = env.query_account(receiver()); - if nonrefundable { - assert_eq!(receiver_before.amount, receiver_after.amount); - assert_eq!(receiver_before.nonrefundable + deposit, receiver_after.nonrefundable); - } else { - assert_eq!(receiver_before.amount + deposit, receiver_after.amount); - assert_eq!(receiver_before.nonrefundable, receiver_after.nonrefundable); - } - } + let gas_cost = outcome.gas_cost(); + assert_eq!(sender_pre_balance - deposit - gas_cost, env.query_balance(sender())); + + let receiver_after = env.query_account(receiver); + let (receiver_expected_amount_after, receiver_expected_non_refundable_after) = if nonrefundable + { + (receiver_before_amount, receiver_before_nonrefundable + deposit) + } else { + (receiver_before_amount + deposit, receiver_before_nonrefundable) + }; + + assert_eq!(receiver_after.amount, receiver_expected_amount_after); + assert_eq!(receiver_after.nonrefundable, receiver_expected_non_refundable_after); + + tx_result +} + +fn delete_account( + env: &mut TestEnv, + signer: &InMemorySigner, +) -> Result { + let actions = vec![Action::DeleteAccount(DeleteAccountAction { beneficiary_id: receiver() })]; + execute_transaction_from_actions(env, actions, &signer, signer.account_id.clone()) +} + +/// Can delete account with non-refundable storage. +#[test] +fn deleting_account_with_non_refundable_storage() { + let mut env = setup_env(); + let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let new_account = InMemorySigner::from_seed( + new_account_id.clone(), + KeyType::ED25519, + new_account_id.as_str(), + ); + let create_account_tx_result = exec_transfer_v2( + &mut env, + signer(), + new_account_id.clone(), + NEAR_BASE, + true, + true, + false, + true, + ); + create_account_tx_result.unwrap().assert_success(); + + let delete_account_tx_result = delete_account(&mut env, &new_account); + delete_account_tx_result.unwrap().assert_success(); + assert!(!account_exists(&mut env, new_account_id)); +} + +/// Non-refundable balance cannot be transferred. +#[test] +fn non_refundable_balance_cannot_be_transferred() { + let mut env = setup_env(); + let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let new_account = InMemorySigner::from_seed( + new_account_id.clone(), + KeyType::ED25519, + new_account_id.as_str(), + ); + // The `new_account` is created with `NEAR_BASE` non-refundable balance. + let create_account_tx_result = exec_transfer_v2( + &mut env, + signer(), + new_account_id.clone(), + NEAR_BASE, + true, + true, + false, + false, + ); + create_account_tx_result.unwrap().assert_success(); + + // Although `new_account` has `NEAR_BASE` balance, it cannot make neither refundable nor non-refundable transfer of 1. + for nonrefundable in [false, true] { + let transfer_tx_result = exec_transfer_v2( + &mut env, + new_account.clone(), + receiver(), + 1, + nonrefundable, + false, + false, + false, + ); + assert_eq!( + transfer_tx_result, + Err(InvalidTxError::NotEnoughBalance { + signer_id: new_account_id.clone(), + balance: 0, + cost: 1, + }), + ); } +} + +/// Non-refundable balance allows to have account with zero balance and more than 1kB of state. +#[test] +fn non_refundable_balance_allows_1kb_state_with_zero_balance() { + let mut env = setup_env(); + let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let tx_result = exec_transfer_v2( + &mut env, + signer(), + new_account_id, + NEAR_BASE / 5, + true, + true, + false, + true, + ); + tx_result.unwrap().assert_success(); +} - status +/// Non-refundable transfer successfully adds non-refundable balance when creating named account. +#[test] +fn non_refundable_transfer_create_named_account() { + let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let tx_result = + exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, false, false); + tx_result.unwrap().assert_success(); +} + +/// Non-refundable transfer successfully adds non-refundable balance when creating NEAR-implicit account. +#[test] +fn non_refundable_transfer_create_near_implicit_account() { + let new_account_id = near_implicit_test_account(); + let tx_result = + exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + tx_result.unwrap().assert_success(); +} + +/// Non-refundable transfer is rejected on existing account. +#[test] +fn reject_non_refundable_transfer_existing_account() { + let tx_result = + exec_transfer_v2(&mut setup_env(), signer(), receiver(), 1, true, false, false, false); + let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; + assert!(matches!( + status, + ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + )) if *account_id == receiver(), + )); +} + +/// Refundable transfer V2 successfully adds balance like a transfer V1. +#[test] +fn transfer_v2() { + exec_transfer_v2(&mut setup_env(), signer(), receiver(), 1, false, false, false, false) + .expect("Transfer V2 should be accepted") + .assert_success(); +} + +/// During the protocol upgrade phase, before the voting completes, we must not +/// include transfer V2 actions on the chain. +/// +/// The correct way to handle it is to reject transaction before they even get +/// into the transaction pool. Hence, we check that an `InvalidTxError` error is +/// returned for older protocol versions. +#[test] +fn reject_transfer_v2_in_older_versions() { + let mut env = setup_env_with_protocol_version(Some( + ProtocolFeature::NonRefundableBalance.protocol_version() - 1, + )); + let tx_result = exec_transfer_v2(&mut env, signer(), receiver(), 1, false, false, false, false); + assert_eq!( + tx_result, + Err(InvalidTxError::ActionsValidation( + ActionsValidationError::UnsupportedProtocolFeature { + protocol_feature: "NonRefundableBalance".to_string(), + version: ProtocolFeature::NonRefundableBalance.protocol_version() + } + )) + ); } diff --git a/integration-tests/src/tests/client/features/wallet_contract.rs b/integration-tests/src/tests/client/features/wallet_contract.rs index 1c43a0dd4eb..0bea3ddb55e 100644 --- a/integration-tests/src/tests/client/features/wallet_contract.rs +++ b/integration-tests/src/tests/client/features/wallet_contract.rs @@ -13,13 +13,10 @@ use near_primitives::transaction::{ TransferAction, }; use near_primitives::utils::derive_eth_implicit_account_id; -use near_primitives::views::{ - FinalExecutionStatus, QueryRequest, QueryResponse, QueryResponseKind, -}; +use near_primitives::views::{FinalExecutionStatus, QueryRequest, QueryResponseKind}; use near_primitives_core::{ account::AccessKey, checked_feature, types::BlockHeight, version::PROTOCOL_VERSION, }; -use near_store::ShardUId; use near_vm_runner::ContractCode; use near_wallet_contract::{wallet_contract, wallet_contract_magic_bytes}; use nearcore::{config::GenesisExt, test_utils::TestEnvNightshadeSetupExt, NEAR_BASE}; @@ -48,24 +45,6 @@ fn check_tx_processing( next_height } -fn view_request(env: &TestEnv, request: QueryRequest) -> QueryResponse { - let head = env.clients[0].chain.head().unwrap(); - let head_block = env.clients[0].chain.get_block(&head.last_block_hash).unwrap(); - env.clients[0] - .runtime_adapter - .query( - ShardUId::single_shard(), - &head_block.chunks()[0].prev_state_root(), - head.height, - 0, - &head.prev_block_hash, - &head.last_block_hash, - head_block.header().epoch_id(), - &request, - ) - .unwrap() -} - /// Tests that ETH-implicit account is created correctly, with Wallet Contract hash. #[test] fn test_eth_implicit_account_creation() { @@ -101,7 +80,7 @@ fn test_eth_implicit_account_creation() { // Verify the ETH-implicit account has zero balance and appropriate code hash. // Check that the account storage fits within zero balance account limit. let request = QueryRequest::ViewAccount { account_id: eth_implicit_account_id.clone() }; - match view_request(&env, request).kind { + match env.query_view(request).unwrap().kind { QueryResponseKind::ViewAccount(view) => { assert_eq!(view.amount, 0); assert_eq!(view.code_hash, *magic_bytes.hash()); @@ -112,7 +91,7 @@ fn test_eth_implicit_account_creation() { // Verify that contract code deployed to the ETH-implicit account is near[wallet contract hash]. let request = QueryRequest::ViewCode { account_id: eth_implicit_account_id }; - match view_request(&env, request).kind { + match env.query_view(request).unwrap().kind { QueryResponseKind::ViewCode(view) => { let contract_code = ContractCode::new(view.code, None); assert_eq!(contract_code.hash(), magic_bytes.hash()); From 9c3b741afb7120379c97d54ce6394dff0205d1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Wed, 3 Jan 2024 15:51:24 +0100 Subject: [PATCH 19/36] Update burnt amount on account deletion --- .../client/features/nonrefundable_transfer.rs | 20 +++++++++++++-- runtime/runtime/src/lib.rs | 25 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 7c417353f2c..d4889ef6230 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -28,7 +28,7 @@ use nearcore::config::GenesisExt; use nearcore::test_utils::TestEnvNightshadeSetupExt; use nearcore::NEAR_BASE; -// Default sender to use in tests of this module. +/// Default sender to use in tests of this module. fn sender() -> AccountId { "test0".parse().unwrap() } @@ -49,7 +49,7 @@ fn setup_env_with_protocol_version(protocol_version: Option) -> if let Some(protocol_version) = protocol_version { genesis.config.protocol_version = protocol_version; } - TestEnv::builder(ChainGenesis::test()) + TestEnv::builder(ChainGenesis::new(&genesis)) .real_epoch_managers(&genesis.config) .nightshade_runtimes(&genesis) .build() @@ -184,6 +184,8 @@ fn deleting_account_with_non_refundable_storage() { KeyType::ED25519, new_account_id.as_str(), ); + // Create account with non-refundable storage. + // Deploy a contract that does not fit within Zero-balance account limit. let create_account_tx_result = exec_transfer_v2( &mut env, signer(), @@ -196,6 +198,20 @@ fn deleting_account_with_non_refundable_storage() { ); create_account_tx_result.unwrap().assert_success(); + // Send some NEAR (refundable) so that the new account is able to pay the gas for its deletion in the next transaction. + let send_money_tx_result = exec_transfer_v2( + &mut env, + signer(), + new_account_id.clone(), + 10u128.pow(20), + false, + false, + false, + false, + ); + send_money_tx_result.unwrap().assert_success(); + + // Delete the new account (that has 1 NEAR of non-refundable balance). let delete_account_tx_result = delete_account(&mut env, &new_account); delete_account_tx_result.unwrap().assert_success(); assert!(!account_exists(&mut env, new_account_id)); diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 8a611f6180b..5362957207a 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -24,9 +24,11 @@ pub use near_primitives::runtime::apply_state::ApplyState; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +use near_primitives::transaction::DeleteAccountAction; use near_primitives::transaction::{ - Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, - SignedTransaction, TransferAction, + Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, + ExecutionStatus, LogEntry, SignedTransaction, TransferAction, }; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ @@ -468,6 +470,10 @@ impl Runtime { _ => unreachable!("given receipt should be an action receipt"), }; let account_id = &receipt.receiver_id; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let account_before_update = get_account(state_update, account_id)?; + // Collecting input data and removing it from the state let promise_results = action_receipt .input_data_ids @@ -544,6 +550,21 @@ impl Runtime { res.index = Some(action_index as u64); break; } + + // We update `other_burnt_amount` statistic with the non-refundable amount being burnt on account deletion. + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + if matches!(action, Action::DeleteAccount(DeleteAccountAction { beneficiary_id: _ })) { + debug_assert!( + account_before_update.is_some(), + "Missing account state from before deletion." + ); + if let Some(ref account_before_deletion) = account_before_update { + stats.other_burnt_amount = safe_add_balance( + stats.other_burnt_amount, + account_before_deletion.nonrefundable(), + )? + } + } } // Going to check balance covers account's storage. From d42f12536e754f4d467286f39ab07256ff568a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Wed, 3 Jan 2024 17:51:42 +0100 Subject: [PATCH 20/36] Eth-implicit non-refundable transfer --- core/primitives-core/src/account.rs | 11 +++++----- core/primitives/src/test_utils.rs | 2 +- core/primitives/src/views.rs | 4 ++-- .../client/features/nonrefundable_transfer.rs | 17 ++++++++++---- nearcore/src/runtime/tests.rs | 3 ++- runtime/runtime/src/actions.rs | 22 +++++++++---------- runtime/runtime/src/balance_checker.rs | 11 +++++----- runtime/runtime/src/config.rs | 4 ++-- runtime/runtime/src/lib.rs | 4 ++-- 9 files changed, 43 insertions(+), 35 deletions(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 06b02644208..231bf7e1faa 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -143,7 +143,6 @@ impl Account { #[inline] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { self.nonrefundable = nonrefundable; } @@ -191,10 +190,10 @@ impl BorshDeserialize for Account { if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { // Account v2 or newer let version_byte = u8::deserialize_reader(rd)?; - // TODO(jakmeier): return proper error instead of panic + // TODO(nonrefundable-transfer) Return proper error instead of panic debug_assert_eq!(version_byte, 2); - // TODO(jakmeier): return proper error instead of panic - let version = AccountVersion::try_from(version_byte).expect("TODO(jakmeier)"); + // TODO(nonrefundable-transfer) Return proper error instead of panic + let version = AccountVersion::try_from(version_byte).expect(""); let amount = u128::deserialize_reader(rd)?; let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; @@ -251,7 +250,7 @@ impl BorshSerialize for Account { // while serializing. But that would break the borsh assumptions // of unique binary representation. AccountVersion::V1 => legacy_account.serialize(writer), - // TODO(jakmeier): Can we do better than this? + // TODO(nonrefundable-transfer): Can we do better than this? // Context: These accounts are serialized in merklized state. I // would really like to avoid migration of the MPT. This here would // keep old accounts in the old format and only allow nonrefundable @@ -263,7 +262,7 @@ impl BorshSerialize for Account { let version = 2u8; BorshSerialize::serialize(&sentinel, writer)?; BorshSerialize::serialize(&version, writer)?; - // TODO(jakmeier): Consider wrapping this in a struct and derive BorshSerialize for it. + // TODO(nonrefundable-transfer): Consider wrapping this in a struct and derive BorshSerialize for it. BorshSerialize::serialize(&self.amount, writer)?; BorshSerialize::serialize(&self.locked, writer)?; BorshSerialize::serialize(&self.code_hash, writer)?; diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 34ef47d4248..a0fac74021f 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -570,7 +570,7 @@ impl FinalExecutionOutcomeView { #[track_caller] /// Check transaction and all transitive receipts for success status. pub fn assert_success(&self) { - assert!(matches!(self.status, FinalExecutionStatus::SuccessValue(_)), "{:?}", self.status); + assert!(matches!(self.status, FinalExecutionStatus::SuccessValue(_))); for (i, receipt) in self.receipts_outcome.iter().enumerate() { assert!( matches!( diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index c1a4dcd8e64..89d0b6bfd61 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1241,7 +1241,7 @@ impl From for ActionView { nonrefundable: false, }, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - // TODO: We lose the information if it was a deprecated + // TODO(nonrefundable-transfer): We lose the information if it was a deprecated // TransferAction or an equivalent refundable TransferActionV2. // Is this good enough? Arguably, the view shouldn't care about it // but this needs to be discussed with consumers of the view. @@ -1287,7 +1287,7 @@ impl TryFrom for Action { } #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] ActionView::Transfer { deposit } => Action::Transfer(TransferAction { deposit }), - // TODO: We always return the new TransferActionV2. + // TODO(nonrefundable-transfer): We always return the new TransferActionV2. // Is this good enough? Must the Action -> View -> Action conversion be lossless? #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] ActionView::Transfer { deposit, nonrefundable } => { diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index d4889ef6230..ffb490cec25 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -13,7 +13,7 @@ use near_crypto::{InMemorySigner, KeyType, PublicKey}; use near_primitives::errors::{ ActionError, ActionErrorKind, ActionsValidationError, InvalidTxError, TxExecutionError, }; -use near_primitives::test_utils::near_implicit_test_account; +use near_primitives::test_utils::{eth_implicit_test_account, near_implicit_test_account}; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, SignedTransaction, TransferActionV2, @@ -55,7 +55,7 @@ fn setup_env_with_protocol_version(protocol_version: Option) -> .build() } -/// Creates a test environment without modifying protocol version. +/// Creates a test environment using default protocol version. fn setup_env() -> TestEnv { setup_env_with_protocol_version(None) } @@ -227,7 +227,7 @@ fn non_refundable_balance_cannot_be_transferred() { KeyType::ED25519, new_account_id.as_str(), ); - // The `new_account` is created with `NEAR_BASE` non-refundable balance. + // The `new_account` is created with 1 NEAR non-refundable balance. let create_account_tx_result = exec_transfer_v2( &mut env, signer(), @@ -240,7 +240,7 @@ fn non_refundable_balance_cannot_be_transferred() { ); create_account_tx_result.unwrap().assert_success(); - // Although `new_account` has `NEAR_BASE` balance, it cannot make neither refundable nor non-refundable transfer of 1. + // Although `new_account` has 1 NEAR balance, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. for nonrefundable in [false, true] { let transfer_tx_result = exec_transfer_v2( &mut env, @@ -299,6 +299,15 @@ fn non_refundable_transfer_create_near_implicit_account() { tx_result.unwrap().assert_success(); } +/// Non-refundable transfer successfully adds non-refundable balance when creating ETH-implicit account. +#[test] +fn non_refundable_transfer_create_eth_implicit_account() { + let new_account_id = eth_implicit_test_account(); + let tx_result = + exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + tx_result.unwrap().assert_success(); +} + /// Non-refundable transfer is rejected on existing account. #[test] fn reject_non_refundable_transfer_existing_account() { diff --git a/nearcore/src/runtime/tests.rs b/nearcore/src/runtime/tests.rs index 2f0e1fda302..9a507a0bcd2 100644 --- a/nearcore/src/runtime/tests.rs +++ b/nearcore/src/runtime/tests.rs @@ -1437,7 +1437,8 @@ fn test_genesis_hash() { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] let block_expected_hash = "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"; - + // TODO(nonrefundable-transfer) Discuss the problem of the changed genesis hash. + // We probably should not change it. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let block_expected_hash = "3Cm2e8aZQuZbbnwWMNES6WnuszR7r2zzhdDhBK1Ft3f1"; diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 5f6ba002c7e..178d7af2347 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -481,6 +481,9 @@ pub(crate) fn action_implicit_account_creation_transfer( ) { *actor_id = account_id.clone(); + let (refundable_balance, nonrefundable_balance) = + if nonrefundable { (0, deposit) } else { (deposit, 0) }; + match account_id.get_account_type() { AccountType::NearImplicitAccount => { let mut access_key = AccessKey::full_access(); @@ -498,17 +501,6 @@ pub(crate) fn action_implicit_account_creation_transfer( // unwrap: here it's safe because the `account_id` has already been determined to be implicit by `get_account_type` let public_key = PublicKey::from_near_implicit_account(account_id).unwrap(); - // TODO(jakmeier): feature flag? - let refundable_balance; - let nonrefundable_balance; - if nonrefundable { - refundable_balance = 0; - nonrefundable_balance = deposit; - } else { - refundable_balance = deposit; - nonrefundable_balance = 0; - } - *account = Some(Account::new( refundable_balance, 0, @@ -535,7 +527,13 @@ pub(crate) fn action_implicit_account_creation_transfer( + magic_bytes.code().len() as u64 + fee_config.storage_usage_config.num_extra_bytes_record; - *account = Some(Account::new(deposit, 0, 0, *magic_bytes.hash(), storage_usage)); + *account = Some(Account::new( + refundable_balance, + 0, + nonrefundable_balance, + *magic_bytes.hash(), + storage_usage, + )); set_code(state_update, account_id.clone(), &magic_bytes); // Precompile Wallet Contract and store result (compiled code or error) in the database. diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index fa8047a8e6e..714067c8512 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -437,7 +437,8 @@ mod tests { let deposit = 1000; let mut initial_state = tries.new_trie_update(ShardUId::single_shard(), root); - // u128::MAX is used as a sentinel value for account version 2 or higher, see https://github.com/near/NEPs/pull/491. + // We use `u128::MAX - 1`, because `u128::MAX` is used as a sentinel value for accounts version 2 or higher. + // See NEP-491 for more details: https://github.com/near/NEPs/pull/491. let alice = account_new(u128::MAX - 1, hash(&[])); let bob = account_new(2u128, hash(&[])); @@ -448,7 +449,7 @@ mod tests { let signer = InMemorySigner::from_seed(alice_id.clone(), KeyType::ED25519, alice_id.as_ref()); - // Sending 2, so that we have an overflow when adding to alice's balance. + // Sending 2 yoctoNEAR, so that we have an overflow when adding to alice's balance. let tx = SignedTransaction::send_money(0, alice_id, bob_id, &signer, 2, CryptoHash::default()); @@ -480,9 +481,9 @@ mod tests { ); } - /// This tests shows what would happen if the total balance becomes u128::MAX, - /// a sentinel value use to distinguish between accounts version 1 and 2+, - /// see https://github.com/near/NEPs/pull/491. + /// This tests shows what would happen if the total balance becomes u128::MAX + /// which is also the sentinel value use to distinguish between accounts version 1 and 2 or higher + /// See NEP-491 for more details: https://github.com/near/NEPs/pull/491. #[test] fn test_total_balance_u128_max() { let tries = TestTriesBuilder::new().build(); diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 1a2554a0e02..328004d4eea 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -104,7 +104,7 @@ pub fn total_send_fees( } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] TransferV2(_) => { - // Note: when stabilizing, merge with branch above + // TODO(nonrefundable-storage) When stabilizing, merge with branch above // Account for implicit account creation transfer_send_fee( fees, @@ -211,7 +211,7 @@ pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] TransferV2(_) => { - // Note: when stabilizing, merge with branch above + // TODO(nonrefundable-storage) When stabilizing, merge with branch above // Account for implicit account creation transfer_exec_fee( fees, diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 5362957207a..ee365802138 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -27,8 +27,8 @@ use near_primitives::state_record::StateRecord; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] use near_primitives::transaction::DeleteAccountAction; use near_primitives::transaction::{ - Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, - ExecutionStatus, LogEntry, SignedTransaction, TransferAction, + Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, + SignedTransaction, TransferAction, }; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ From 43d56b37e76d6e66a140df25f10c811b22fc31b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Thu, 4 Jan 2024 16:29:00 +0100 Subject: [PATCH 21/36] Introduce ReserveStorage action instead of deprecating the Transfer action --- chain/rosetta-rpc/src/adapters/mod.rs | 8 +- core/primitives-core/src/account.rs | 59 +++++++++----- core/primitives/src/action/mod.rs | 9 +-- core/primitives/src/transaction.rs | 2 +- core/primitives/src/views.rs | 41 ++++------ .../client/features/nonrefundable_transfer.rs | 76 ++++++++----------- nearcore/src/runtime/tests.rs | 2 +- runtime/runtime/src/actions.rs | 31 +++----- runtime/runtime/src/config.rs | 4 +- runtime/runtime/src/lib.rs | 23 +++--- runtime/runtime/src/verifier.rs | 2 +- tools/state-viewer/src/contract_accounts.rs | 4 +- 12 files changed, 123 insertions(+), 138 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index a32d4367f2d..727b970e212 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -326,8 +326,6 @@ impl From for Vec { ); } - // Note: Both refundable and non-refundable transfers are considered as available balance. - // (TODO: ensure final decision for NEP-491 aligns with that!) near_primitives::transaction::Action::Transfer(TransferAction { deposit }) => { let transfer_amount = crate::models::Amount::from_yoctonear(deposit); @@ -354,9 +352,10 @@ impl From for Vec { ), ); } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - near_primitives::transaction::Action::TransferV2(action) => { - // TODO(protocol_feature_nonrefundable_transfer_nep491): merge with branch above on stabilization + // Note: Both refundable and non-refundable transfers are considered as available balance. + near_primitives::transaction::Action::ReserveStorage(action) => { let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); let sender_transfer_operation_id = @@ -382,6 +381,7 @@ impl From for Vec { ), ); } + near_primitives::transaction::Action::Stake(action) => { operations.push( validated_operations::StakeOperation { diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 231bf7e1faa..8c28e6b0dfd 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -182,6 +182,15 @@ struct LegacyAccount { storage_usage: StorageUsage, } +#[derive(BorshSerialize)] +struct AccountV2 { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, + nonrefundable: Balance, +} + impl BorshDeserialize for Account { fn deserialize_reader(rd: &mut R) -> io::Result { // The first value of all Account serialization formats is a u128, @@ -190,10 +199,26 @@ impl BorshDeserialize for Account { if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { // Account v2 or newer let version_byte = u8::deserialize_reader(rd)?; - // TODO(nonrefundable-transfer) Return proper error instead of panic - debug_assert_eq!(version_byte, 2); - // TODO(nonrefundable-transfer) Return proper error instead of panic - let version = AccountVersion::try_from(version_byte).expect(""); + if version_byte != 2 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Error deserializing account: version {} does not equal 2", + version_byte + ), + )); + } + + let version = AccountVersion::try_from(version_byte).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Error deserializing account: invalid account version {}", + version_byte + ), + ) + })?; + let amount = u128::deserialize_reader(rd)?; let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; @@ -246,29 +271,29 @@ impl BorshSerialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] { match self.version { - // Note: It might be tempting to lazily convert old V1 to V2 + // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 // while serializing. But that would break the borsh assumptions // of unique binary representation. AccountVersion::V1 => legacy_account.serialize(writer), - // TODO(nonrefundable-transfer): Can we do better than this? - // Context: These accounts are serialized in merklized state. I - // would really like to avoid migration of the MPT. This here would - // keep old accounts in the old format and only allow nonrefundable - // storage on new accounts. + // Note(jakmeier): These accounts are serialized in merklized state. + // I would really like to avoid migration of the MPT. + // This here would keep old accounts in the old format + // and only allow nonrefundable storage on new accounts. AccountVersion::V2 => { + let account = AccountV2 { + amount: self.amount, + locked: self.locked, + code_hash: self.code_hash, + storage_usage: self.storage_usage, + nonrefundable: self.nonrefundable, + }; let sentinel = Account::SERIALIZATION_SENTINEL; // For now a constant, but if we need V3 later we can use this // field instead of sentinel magic. let version = 2u8; BorshSerialize::serialize(&sentinel, writer)?; BorshSerialize::serialize(&version, writer)?; - // TODO(nonrefundable-transfer): Consider wrapping this in a struct and derive BorshSerialize for it. - BorshSerialize::serialize(&self.amount, writer)?; - BorshSerialize::serialize(&self.locked, writer)?; - BorshSerialize::serialize(&self.code_hash, writer)?; - BorshSerialize::serialize(&self.storage_usage, writer)?; - BorshSerialize::serialize(&self.nonrefundable, writer)?; - Ok(()) + account.serialize(writer) } } } diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index ad35c77565d..ae3eb64aaed 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -163,11 +163,9 @@ pub struct TransferAction { serde::Deserialize, )] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -pub struct TransferActionV2 { +pub struct ReserveStorageAction { #[serde(with = "dec_format")] pub deposit: Balance, - /// If this flag is set, the balance will be added to the receiver's non-refundable balance. - pub nonrefundable: bool, } #[derive( @@ -189,7 +187,6 @@ pub enum Action { /// Sets a Wasm code to a receiver_id DeployContract(DeployContractAction), FunctionCall(Box), - /// To be deprecated with NEP-491 but kept for backwards-compatibility. Transfer(TransferAction), Stake(Box), AddKey(Box), @@ -197,7 +194,7 @@ pub enum Action { DeleteAccount(DeleteAccountAction), Delegate(Box), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - TransferV2(Box), + ReserveStorage(ReserveStorageAction), } const _: () = assert!( cfg!(not(target_pointer_width = "64")) || std::mem::size_of::() == 32, @@ -216,7 +213,7 @@ impl Action { Action::FunctionCall(a) => a.deposit, Action::Transfer(a) => a.deposit, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(a) => a.deposit, + Action::ReserveStorage(a) => a.deposit, _ => 0, } } diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 553cb9264f5..c332edb7757 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -15,7 +15,7 @@ use std::fmt; use std::hash::{Hash, Hasher}; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -pub use crate::action::TransferActionV2; +pub use crate::action::ReserveStorageAction; pub use crate::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 89d0b6bfd61..d925f3853b0 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -23,13 +23,13 @@ use crate::sharding::{ ChunkHash, ShardChunk, ShardChunkHeader, ShardChunkHeaderInner, ShardChunkHeaderInnerV2, ShardChunkHeaderV3, }; -#[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] -use crate::transaction::TransferAction; +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +use crate::transaction::ReserveStorageAction; use crate::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithIdAndProof, ExecutionStatus, FunctionCallAction, PartialExecutionOutcome, PartialExecutionStatus, - SignedTransaction, StakeAction, + SignedTransaction, StakeAction, TransferAction, }; use crate::types::{ @@ -1196,9 +1196,11 @@ pub enum ActionView { Transfer { #[serde(with = "dec_format")] deposit: Balance, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - #[cfg_attr(feature = "protocol_feature_nonrefundable_transfer_nep491", serde(default))] - nonrefundable: bool, + }, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + ReserveStorage { + #[serde(with = "dec_format")] + deposit: Balance, }, Stake { #[serde(with = "dec_format")] @@ -1235,20 +1237,11 @@ impl From for ActionView { gas: action.gas, deposit: action.deposit, }, - Action::Transfer(action) => ActionView::Transfer { - deposit: action.deposit, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: false, - }, + Action::Transfer(action) => ActionView::Transfer { deposit: action.deposit }, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - // TODO(nonrefundable-transfer): We lose the information if it was a deprecated - // TransferAction or an equivalent refundable TransferActionV2. - // Is this good enough? Arguably, the view shouldn't care about it - // but this needs to be discussed with consumers of the view. - Action::TransferV2(action) => ActionView::Transfer { - deposit: action.deposit, - nonrefundable: action.nonrefundable, - }, + Action::ReserveStorage(action) => { + ActionView::ReserveStorage { deposit: action.deposit } + } Action::Stake(action) => { ActionView::Stake { stake: action.stake, public_key: action.public_key } } @@ -1285,16 +1278,10 @@ impl TryFrom for Action { deposit, })) } - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] ActionView::Transfer { deposit } => Action::Transfer(TransferAction { deposit }), - // TODO(nonrefundable-transfer): We always return the new TransferActionV2. - // Is this good enough? Must the Action -> View -> Action conversion be lossless? #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ActionView::Transfer { deposit, nonrefundable } => { - Action::TransferV2(Box::new(crate::transaction::TransferActionV2 { - deposit, - nonrefundable, - })) + ActionView::ReserveStorage { deposit } => { + Action::ReserveStorage(ReserveStorageAction { deposit }) } ActionView::Stake { stake, public_key } => { Action::Stake(Box::new(StakeAction { stake, public_key })) diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index ffb490cec25..b11215d6cf9 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -2,7 +2,7 @@ //! accounts storage staking balance without that someone being able to run off //! with the money. //! -//! This feature introduces TransferV2 +//! This feature introduces the ReserveStorage action. //! //! NEP: https://github.com/near/NEPs/pull/491 @@ -16,7 +16,7 @@ use near_primitives::errors::{ use near_primitives::test_utils::{eth_implicit_test_account, near_implicit_test_account}; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, - SignedTransaction, TransferActionV2, + ReserveStorageAction, SignedTransaction, TransferAction, }; use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; @@ -100,11 +100,11 @@ fn execute_transaction_from_actions( tx_result } -/// Submits a transfer V2 action. +/// Submits a transfer (either regular or non-refundable). /// /// This methods checks that the balance is subtracted from the sender and added /// to the receiver, if the status was ok. No checks are done on an error. -fn exec_transfer_v2( +fn exec_transfer( env: &mut TestEnv, signer: InMemorySigner, receiver: AccountId, @@ -123,6 +123,7 @@ fn exec_transfer_v2( }; let mut actions = vec![]; + if account_creation && !implicit_account_creation { actions.push(Action::CreateAccount(CreateAccountAction {})); actions.push(Action::AddKey(Box::new(AddKeyAction { @@ -130,7 +131,13 @@ fn exec_transfer_v2( access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, }))); } - actions.push(Action::TransferV2(Box::new(TransferActionV2 { deposit, nonrefundable }))); + + if nonrefundable { + actions.push(Action::ReserveStorage(ReserveStorageAction { deposit })); + } else { + actions.push(Action::Transfer(TransferAction { deposit })); + } + if deploy_contract { let contract = near_test_contracts::sized_contract(1500 as usize); actions.push(Action::DeployContract(DeployContractAction { code: contract.to_vec() })) @@ -186,7 +193,7 @@ fn deleting_account_with_non_refundable_storage() { ); // Create account with non-refundable storage. // Deploy a contract that does not fit within Zero-balance account limit. - let create_account_tx_result = exec_transfer_v2( + let create_account_tx_result = exec_transfer( &mut env, signer(), new_account_id.clone(), @@ -199,7 +206,7 @@ fn deleting_account_with_non_refundable_storage() { create_account_tx_result.unwrap().assert_success(); // Send some NEAR (refundable) so that the new account is able to pay the gas for its deletion in the next transaction. - let send_money_tx_result = exec_transfer_v2( + let send_money_tx_result = exec_transfer( &mut env, signer(), new_account_id.clone(), @@ -228,7 +235,7 @@ fn non_refundable_balance_cannot_be_transferred() { new_account_id.as_str(), ); // The `new_account` is created with 1 NEAR non-refundable balance. - let create_account_tx_result = exec_transfer_v2( + let create_account_tx_result = exec_transfer( &mut env, signer(), new_account_id.clone(), @@ -240,9 +247,9 @@ fn non_refundable_balance_cannot_be_transferred() { ); create_account_tx_result.unwrap().assert_success(); - // Although `new_account` has 1 NEAR balance, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. + // Although `new_account` has 1 NEAR non-refundable balance, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. for nonrefundable in [false, true] { - let transfer_tx_result = exec_transfer_v2( + let transfer_tx_result = exec_transfer( &mut env, new_account.clone(), receiver(), @@ -252,14 +259,13 @@ fn non_refundable_balance_cannot_be_transferred() { false, false, ); - assert_eq!( - transfer_tx_result, - Err(InvalidTxError::NotEnoughBalance { - signer_id: new_account_id.clone(), - balance: 0, - cost: 1, - }), - ); + match transfer_tx_result { + Err(InvalidTxError::NotEnoughBalance { signer_id, balance, .. }) => { + assert_eq!(signer_id, new_account_id); + assert_eq!(balance, 0); + } + _ => panic!("Expected NotEnoughBalance error"), + } } } @@ -268,16 +274,8 @@ fn non_refundable_balance_cannot_be_transferred() { fn non_refundable_balance_allows_1kb_state_with_zero_balance() { let mut env = setup_env(); let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); - let tx_result = exec_transfer_v2( - &mut env, - signer(), - new_account_id, - NEAR_BASE / 5, - true, - true, - false, - true, - ); + let tx_result = + exec_transfer(&mut env, signer(), new_account_id, NEAR_BASE / 5, true, true, false, true); tx_result.unwrap().assert_success(); } @@ -286,7 +284,7 @@ fn non_refundable_balance_allows_1kb_state_with_zero_balance() { fn non_refundable_transfer_create_named_account() { let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); let tx_result = - exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, false, false); + exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, false, false); tx_result.unwrap().assert_success(); } @@ -295,7 +293,7 @@ fn non_refundable_transfer_create_named_account() { fn non_refundable_transfer_create_near_implicit_account() { let new_account_id = near_implicit_test_account(); let tx_result = - exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); tx_result.unwrap().assert_success(); } @@ -304,7 +302,7 @@ fn non_refundable_transfer_create_near_implicit_account() { fn non_refundable_transfer_create_eth_implicit_account() { let new_account_id = eth_implicit_test_account(); let tx_result = - exec_transfer_v2(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); tx_result.unwrap().assert_success(); } @@ -312,7 +310,7 @@ fn non_refundable_transfer_create_eth_implicit_account() { #[test] fn reject_non_refundable_transfer_existing_account() { let tx_result = - exec_transfer_v2(&mut setup_env(), signer(), receiver(), 1, true, false, false, false); + exec_transfer(&mut setup_env(), signer(), receiver(), 1, true, false, false, false); let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; assert!(matches!( status, @@ -322,26 +320,18 @@ fn reject_non_refundable_transfer_existing_account() { )); } -/// Refundable transfer V2 successfully adds balance like a transfer V1. -#[test] -fn transfer_v2() { - exec_transfer_v2(&mut setup_env(), signer(), receiver(), 1, false, false, false, false) - .expect("Transfer V2 should be accepted") - .assert_success(); -} - /// During the protocol upgrade phase, before the voting completes, we must not -/// include transfer V2 actions on the chain. +/// include non-refundable transfer actions on the chain. /// /// The correct way to handle it is to reject transaction before they even get /// into the transaction pool. Hence, we check that an `InvalidTxError` error is /// returned for older protocol versions. #[test] -fn reject_transfer_v2_in_older_versions() { +fn reject_non_refundable_transfer_in_older_versions() { let mut env = setup_env_with_protocol_version(Some( ProtocolFeature::NonRefundableBalance.protocol_version() - 1, )); - let tx_result = exec_transfer_v2(&mut env, signer(), receiver(), 1, false, false, false, false); + let tx_result = exec_transfer(&mut env, signer(), receiver(), 1, true, false, false, false); assert_eq!( tx_result, Err(InvalidTxError::ActionsValidation( diff --git a/nearcore/src/runtime/tests.rs b/nearcore/src/runtime/tests.rs index 9a507a0bcd2..c1badcbd4bb 100644 --- a/nearcore/src/runtime/tests.rs +++ b/nearcore/src/runtime/tests.rs @@ -1437,7 +1437,7 @@ fn test_genesis_hash() { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] let block_expected_hash = "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"; - // TODO(nonrefundable-transfer) Discuss the problem of the changed genesis hash. + // TODO(nonrefundable-storage) Discuss the problem of the changed genesis hash. // We probably should not change it. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let block_expected_hash = "3Cm2e8aZQuZbbnwWMNES6WnuszR7r2zzhdDhBK1Ft3f1"; diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 178d7af2347..a687ca55e0e 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -955,7 +955,7 @@ pub(crate) fn check_actor_permissions( Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), Action::Delegate(_) => (), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(_) => (), + Action::ReserveStorage(_) => (), }; Ok(()) } @@ -1003,28 +1003,16 @@ pub(crate) fn check_account_existence( } Action::Transfer(_) => { if account.is_none() { - return if config.wasm_config.implicit_account_creation - && is_the_only_action - && account_is_implicit(account_id, config.wasm_config.eth_implicit_accounts) - && !is_refund - { - // OK. It's implicit account creation. - // Notes: - // - The transfer action has to be the only action in the transaction to avoid - // abuse by hijacking this account with other public keys or contracts. - // - Refunds don't automatically create accounts, because refunds are free and - // we don't want some type of abuse. - // - Account deletion with beneficiary creates a refund, so it'll not create a - // new account. - Ok(()) - } else { - Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone() } - .into()) - }; + return check_transfer_to_nonexisting_account( + config, + is_the_only_action, + account_id, + is_refund, + ); } } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(transfer) => { + Action::ReserveStorage(_) => { if account.is_none() { return check_transfer_to_nonexisting_account( config, @@ -1032,7 +1020,7 @@ pub(crate) fn check_account_existence( account_id, is_refund, ); - } else if transfer.nonrefundable && !receipt_starts_with_create_account { + } else if !receipt_starts_with_create_account { // If the account already existed before the current receipt, // non-refundable transfer is not allowed. But for named // accounts, it could be that the account was created in this @@ -1073,7 +1061,6 @@ pub(crate) fn check_account_existence( Ok(()) } -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] fn check_transfer_to_nonexisting_account( config: &RuntimeConfig, is_the_only_action: bool, diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 328004d4eea..60d8867f73b 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -103,7 +103,7 @@ pub fn total_send_fees( ) } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - TransferV2(_) => { + ReserveStorage(_) => { // TODO(nonrefundable-storage) When stabilizing, merge with branch above // Account for implicit account creation transfer_send_fee( @@ -210,7 +210,7 @@ pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId ) } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - TransferV2(_) => { + ReserveStorage(_) => { // TODO(nonrefundable-storage) When stabilizing, merge with branch above // Account for implicit account creation transfer_exec_fee( diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index ee365802138..09431a21d61 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -24,12 +24,12 @@ pub use near_primitives::runtime::apply_state::ApplyState; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::sandbox::state_patch::SandboxStatePatch; use near_primitives::state_record::StateRecord; -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -use near_primitives::transaction::DeleteAccountAction; use near_primitives::transaction::{ Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, TransferAction, }; +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +use near_primitives::transaction::{DeleteAccountAction, ReserveStorageAction}; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas, @@ -370,11 +370,10 @@ impl Runtime { )?; } Action::Transfer(TransferAction { deposit }) => { - let nonrefundable = false; action_transfer_or_implicit_account_creation( account, *deposit, - nonrefundable, + false, is_refund, action_receipt, receipt, @@ -384,11 +383,11 @@ impl Runtime { )?; } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(transfer) => { + Action::ReserveStorage(ReserveStorageAction { deposit }) => { action_transfer_or_implicit_account_creation( account, - transfer.deposit, - transfer.nonrefundable, + *deposit, + true, is_refund, action_receipt, receipt, @@ -554,14 +553,12 @@ impl Runtime { // We update `other_burnt_amount` statistic with the non-refundable amount being burnt on account deletion. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] if matches!(action, Action::DeleteAccount(DeleteAccountAction { beneficiary_id: _ })) { - debug_assert!( - account_before_update.is_some(), - "Missing account state from before deletion." - ); - if let Some(ref account_before_deletion) = account_before_update { + // The `account_before_update` can be None if the account is both created and deleted within + // a single action receipt (see `test_create_account_add_key_call_delete_key_delete_account`). + if let Some(ref account_before_update) = account_before_update { stats.other_burnt_amount = safe_add_balance( stats.other_burnt_amount, - account_before_deletion.nonrefundable(), + account_before_update.nonrefundable(), )? } } diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 1b62a2653e2..e39cf7907ed 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -400,7 +400,7 @@ pub fn validate_action( Action::FunctionCall(a) => validate_function_call_action(limit_config, a), Action::Transfer(_) => Ok(()), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::TransferV2(_) => { + Action::ReserveStorage(_) => { check_feature_enabled(ProtocolFeature::NonRefundableBalance, current_protocol_version) } Action::Stake(a) => validate_stake_action(a), diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index 013e60967bd..ee939c79436 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -128,6 +128,8 @@ pub(crate) enum ActionType { DeployContract, FunctionCall, Transfer, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + ReserveStorage, Stake, AddKey, DeleteKey, @@ -336,7 +338,7 @@ fn try_find_actions_spawned_by_receipt( #[cfg( feature = "protocol_feature_nonrefundable_transfer_nep491" )] - Action::TransferV2(_) => ActionType::Transfer, + Action::ReserveStorage(_) => ActionType::ReserveStorage, Action::Stake(_) => ActionType::Stake, Action::AddKey(_) => ActionType::AddKey, Action::DeleteKey(_) => ActionType::DeleteKey, From 9e8948cbed6ccd011204b149029a034e2c2475f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 9 Jan 2024 15:22:17 +0100 Subject: [PATCH 22/36] Fix the mainnet genesis hash issue --- core/primitives-core/src/account.rs | 445 +++++++++++++++++++++------- nearcore/src/runtime/tests.rs | 10 +- runtime/runtime/src/actions.rs | 21 +- 3 files changed, 358 insertions(+), 118 deletions(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 8c28e6b0dfd..71c0eccd7c8 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -38,9 +38,9 @@ impl TryFrom for AccountVersion { } } -/// Per account information stored in the state. -#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct Account { +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +#[derive(serde::Serialize, PartialEq, Eq, Debug, Clone)] +pub struct StandardAccount { /// The total not locked, refundable tokens. #[serde(with = "dec_format")] amount: Balance, @@ -48,8 +48,6 @@ pub struct Account { #[serde(with = "dec_format")] locked: Balance, /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - #[serde(default = "Account::default_nonrefundable")] #[serde(with = "dec_format")] nonrefundable: Balance, /// Hash of the code stored in the storage for this account. @@ -57,6 +55,67 @@ pub struct Account { /// Storage used by the given account, includes account id, this struct, access keys and other data. storage_usage: StorageUsage, /// Version of Account in re migrations and similar + #[serde(default)] + version: AccountVersion, +} + +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +impl StandardAccount { + #[inline] + pub fn nonrefundable(&self) -> Balance { + self.nonrefundable + } + + #[inline] + pub fn version(&self) -> AccountVersion { + self.version + } + + #[inline] + pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { + self.nonrefundable = nonrefundable; + } + + #[inline] + pub fn set_version(&mut self, version: AccountVersion) { + self.version = version; + } +} + +#[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] +#[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + derive(BorshDeserialize) +)] +pub struct LegacyAccount { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, +} + +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +/// Per account information stored in the state. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Account { + LegacyAccount(LegacyAccount), + Account(StandardAccount), +} + +#[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] +#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Account { + /// The total not locked, refundable tokens. + #[serde(with = "dec_format")] + amount: Balance, + /// The amount locked due to staking. + #[serde(with = "dec_format")] + locked: Balance, + /// Hash of the code stored in the storage for this account. + code_hash: CryptoHash, + /// Storage used by the given account, includes account id, this struct, access keys and other data. + storage_usage: StorageUsage, + /// Version of Account in re migrations and similar /// /// Note(jakmeier): Why does this exist? We only have one version right now /// and the code doesn't allow adding a new version at all since this field @@ -82,113 +141,200 @@ impl Account { storage_usage: StorageUsage, ) -> Self { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - assert_eq!(nonrefundable, 0); - Account { + { + assert_eq!(nonrefundable, 0); + Account { amount, locked, code_hash, storage_usage, version: AccountVersion::default() } + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Self::Account(StandardAccount { amount, locked, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, code_hash, storage_usage, version: AccountVersion::default(), - } + }) } #[inline] pub fn amount(&self) -> Balance { - self.amount + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.amount + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.amount, + Account::Account(account) => account.amount, + } } #[inline] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn nonrefundable(&self) -> Balance { - self.nonrefundable - } - - fn default_nonrefundable() -> Balance { - 0 + match self { + Account::LegacyAccount(_) => 0, + Account::Account(account) => account.nonrefundable(), + } } #[inline] #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] pub fn nonrefundable(&self) -> Balance { - Self::default_nonrefundable() + 0 } #[inline] pub fn locked(&self) -> Balance { - self.locked + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.locked + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.locked, + Account::Account(account) => account.locked, + } } #[inline] pub fn code_hash(&self) -> CryptoHash { - self.code_hash + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.code_hash + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.code_hash, + Account::Account(account) => account.code_hash, + } } #[inline] pub fn storage_usage(&self) -> StorageUsage { - self.storage_usage - } + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.storage_usage + } - #[inline] - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn version(&self) -> AccountVersion { - self.version + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.storage_usage, + Account::Account(account) => account.storage_usage, + } } #[inline] pub fn set_amount(&mut self, amount: Balance) { - self.amount = amount; - } + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.amount = amount; + } - #[inline] - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { - self.nonrefundable = nonrefundable; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.amount = amount, + Account::Account(account) => account.amount = amount, + } } #[inline] pub fn set_locked(&mut self, locked: Balance) { - self.locked = locked; + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.locked = locked; + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.locked = locked, + Account::Account(account) => account.locked = locked, + } } #[inline] pub fn set_code_hash(&mut self, code_hash: CryptoHash) { - self.code_hash = code_hash; + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.code_hash = code_hash; + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.code_hash = code_hash, + Account::Account(account) => account.code_hash = code_hash, + } } #[inline] pub fn set_storage_usage(&mut self, storage_usage: StorageUsage) { - self.storage_usage = storage_usage; - } + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + self.storage_usage = storage_usage; + } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn set_version(&mut self, version: AccountVersion) { - self.version = version; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match self { + Account::LegacyAccount(account) => account.storage_usage = storage_usage, + Account::Account(account) => account.storage_usage = storage_usage, + } } } -/// Note(jakmeier): Even though this is called "legacy", it looks like this is -/// the one and only serialization format of Accounts currently in use. -#[derive(BorshSerialize)] -#[cfg_attr( - not(feature = "protocol_feature_nonrefundable_transfer_nep491"), - derive(BorshDeserialize) -)] -struct LegacyAccount { - amount: Balance, - locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +impl serde::Serialize for Account { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Account::LegacyAccount(account) => serde::Serialize::serialize(account, serializer), + Account::Account(account) => serde::Serialize::serialize(account, serializer), + } + } } -#[derive(BorshSerialize)] -struct AccountV2 { - amount: Balance, - locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, - nonrefundable: Balance, +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +impl<'de> serde::Deserialize<'de> for Account { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct AccountData { + #[serde(with = "dec_format")] + amount: Balance, + #[serde(with = "dec_format")] + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, + #[serde(default, with = "dec_format")] + nonrefundable: Option, + } + + let account_data = AccountData::deserialize(deserializer)?; + + match account_data.nonrefundable { + Some(nonrefundable) => Ok(Account::Account(StandardAccount { + amount: account_data.amount, + locked: account_data.locked, + code_hash: account_data.code_hash, + storage_usage: account_data.storage_usage, + nonrefundable: nonrefundable, + version: AccountVersion::V2, + })), + None => Ok(Account::LegacyAccount(LegacyAccount { + amount: account_data.amount, + locked: account_data.locked, + code_hash: account_data.code_hash, + storage_usage: account_data.storage_usage, + })), + } + } } impl BorshDeserialize for Account { @@ -226,30 +372,46 @@ impl BorshDeserialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let nonrefundable = u128::deserialize_reader(rd)?; - Ok(Account { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + Ok(Account { amount, locked, code_hash, storage_usage, version }) + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Ok(Account::Account(StandardAccount { amount, locked, code_hash, storage_usage, version, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, - }) + })) } else { // Account v1 let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; let storage_usage = StorageUsage::deserialize_reader(rd)?; - Ok(Account { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + Ok(Account { + amount: sentinel_or_amount, + locked, + code_hash, + storage_usage, + version: AccountVersion::V1, + }) + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + Ok(Account::Account(StandardAccount { amount: sentinel_or_amount, locked, code_hash, storage_usage, version: AccountVersion::V1, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable: 0, - }) + })) } } } @@ -257,10 +419,10 @@ impl BorshDeserialize for Account { impl BorshSerialize for Account { fn serialize(&self, writer: &mut W) -> io::Result<()> { let legacy_account = LegacyAccount { - amount: self.amount, - locked: self.locked, - code_hash: self.code_hash, - storage_usage: self.storage_usage, + amount: self.amount(), + locked: self.locked(), + code_hash: self.code_hash(), + storage_usage: self.storage_usage(), }; #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] @@ -270,31 +432,43 @@ impl BorshSerialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] { - match self.version { + match self { + Account::LegacyAccount(_) => legacy_account.serialize(writer), // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 // while serializing. But that would break the borsh assumptions // of unique binary representation. - AccountVersion::V1 => legacy_account.serialize(writer), - // Note(jakmeier): These accounts are serialized in merklized state. - // I would really like to avoid migration of the MPT. - // This here would keep old accounts in the old format - // and only allow nonrefundable storage on new accounts. - AccountVersion::V2 => { - let account = AccountV2 { - amount: self.amount, - locked: self.locked, - code_hash: self.code_hash, - storage_usage: self.storage_usage, - nonrefundable: self.nonrefundable, - }; - let sentinel = Account::SERIALIZATION_SENTINEL; - // For now a constant, but if we need V3 later we can use this - // field instead of sentinel magic. - let version = 2u8; - BorshSerialize::serialize(&sentinel, writer)?; - BorshSerialize::serialize(&version, writer)?; - account.serialize(writer) - } + Account::Account(account) => match account.version() { + AccountVersion::V1 => legacy_account.serialize(writer), + // Note(jakmeier): These accounts are serialized in merklized state. + // I would really like to avoid migration of the MPT. + // This here would keep old accounts in the old format + // and only allow nonrefundable storage on new accounts. + AccountVersion::V2 => { + #[derive(BorshSerialize)] + struct AccountV2 { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, + nonrefundable: Balance, + } + + let account = AccountV2 { + amount: self.amount(), + locked: self.locked(), + code_hash: self.code_hash(), + storage_usage: self.storage_usage(), + nonrefundable: self.nonrefundable(), + }; + let sentinel = Account::SERIALIZATION_SENTINEL; + // For now a constant, but if we need V3 later we can use this + // field instead of sentinel magic. + let version = 2u8; + BorshSerialize::serialize(&sentinel, writer)?; + BorshSerialize::serialize(&version, writer)?; + account.serialize(writer) + } + }, } } } @@ -399,35 +573,102 @@ mod tests { use super::*; #[test] - fn test_account_serialization() { - let acc = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); - let bytes = borsh::to_vec(&acc).unwrap(); - if cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491") { - expect_test::expect!("HaZPNG4KpXQ9Mre4PAA83V5usqXsA4zy4vMwSXBiBcQv") - } else { - expect_test::expect!("EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ") + fn test_legacy_account_serde_serialization() { + let old_account = LegacyAccount { + amount: 1_000_000, + locked: 1_000_000, + code_hash: CryptoHash::default(), + storage_usage: 100, + }; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let old_account = Account::LegacyAccount(old_account); + + let serialized_account = serde_json::to_string(&old_account).unwrap(); + let new_account: Account = serde_json::from_str(&serialized_account).unwrap(); + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + assert_eq!(new_account.amount(), old_account.amount); + assert_eq!(new_account.locked(), old_account.locked); + assert_eq!(new_account.code_hash(), old_account.code_hash); + assert_eq!(new_account.storage_usage(), old_account.storage_usage); + assert_eq!(new_account.version, AccountVersion::V1); + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match new_account.clone() { + Account::Account(_) => { + panic!("Expected LegacyAccount, but found StandardAccount") + } + Account::LegacyAccount(account) => { + assert_eq!(account, account); + } } - .assert_eq(&hash(&bytes).to_string()); + + let new_serialized_account = serde_json::to_string(&new_account).unwrap(); + let deserialized_account: Account = serde_json::from_str(&new_serialized_account).unwrap(); + assert_eq!(deserialized_account, new_account); } #[test] - fn test_account_deserialization() { + fn test_legacy_account_borsh_serialization() { let old_account = LegacyAccount { amount: 100, locked: 200, code_hash: CryptoHash::default(), storage_usage: 300, }; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let old_account = Account::LegacyAccount(old_account); + let mut old_bytes = &borsh::to_vec(&old_account).unwrap()[..]; let new_account = ::deserialize(&mut old_bytes).unwrap(); - assert_eq!(new_account.amount, old_account.amount); - assert_eq!(new_account.locked, old_account.locked); - assert_eq!(new_account.code_hash, old_account.code_hash); - assert_eq!(new_account.storage_usage, old_account.storage_usage); - assert_eq!(new_account.version, AccountVersion::V1); + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + { + assert_eq!(new_account.amount(), old_account.amount); + assert_eq!(new_account.locked(), old_account.locked); + assert_eq!(new_account.code_hash(), old_account.code_hash); + assert_eq!(new_account.storage_usage(), old_account.storage_usage); + assert_eq!(new_account.version, AccountVersion::V1); + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + match new_account { + Account::Account(ref acount) => { + assert_eq!(acount.version, AccountVersion::V1); + } + Account::LegacyAccount(_) => { + panic!("Expected StandardAccount, but found LegacyAccount") + } + } + let mut new_bytes = &borsh::to_vec(&new_account).unwrap()[..]; let deserialized_account = ::deserialize(&mut new_bytes).unwrap(); assert_eq!(deserialized_account, new_account); } + + #[test] + fn test_account_serde_serialization() { + let account = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); + let serialized_account = serde_json::to_string(&account).unwrap(); + let deserialized_account: Account = serde_json::from_str(&serialized_account).unwrap(); + assert_eq!(deserialized_account, account); + } + + #[test] + fn test_account_borsh_serialization() { + let account = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); + let serialized_account = borsh::to_vec(&account).unwrap(); + if cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491") { + expect_test::expect!("HaZPNG4KpXQ9Mre4PAA83V5usqXsA4zy4vMwSXBiBcQv") + } else { + expect_test::expect!("EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ") + } + .assert_eq(&hash(&serialized_account).to_string()); + let deserialized_account = + ::deserialize(&mut &serialized_account[..]).unwrap(); + assert_eq!(deserialized_account, account); + } } diff --git a/nearcore/src/runtime/tests.rs b/nearcore/src/runtime/tests.rs index c1badcbd4bb..e426d997aca 100644 --- a/nearcore/src/runtime/tests.rs +++ b/nearcore/src/runtime/tests.rs @@ -1434,15 +1434,7 @@ fn test_genesis_hash() { let block = Chain::make_genesis_block(epoch_manager.as_ref(), runtime.as_ref(), &chain_genesis) .unwrap(); - - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - let block_expected_hash = "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"; - // TODO(nonrefundable-storage) Discuss the problem of the changed genesis hash. - // We probably should not change it. - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let block_expected_hash = "3Cm2e8aZQuZbbnwWMNES6WnuszR7r2zzhdDhBK1Ft3f1"; - - assert_eq!(block.header().hash().to_string(), block_expected_hash); + assert_eq!(block.header().hash().to_string(), "EPnLgE7iEq9s7yTkos96M3cWymH5avBAPm3qx3NXqR8H"); let epoch_manager = EpochManager::new_from_genesis_config(store, &genesis.config).unwrap(); let epoch_info = epoch_manager.get_epoch_info(&EpochId::default()).unwrap(); diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index a687ca55e0e..70ee119c3b9 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -405,13 +405,20 @@ pub(crate) fn action_transfer( if nonrefundable { assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else( - || { - StorageError::StorageInconsistentState( - "Non-refundable account balance integer overflow".to_string(), - ) - }, - )?); + match account { + // This path cannot happen. It is rejected by `check_account_existence` with + // an error `NonRefundableBalanceToExistingAccount` as legacy account must already exist. + Account::LegacyAccount(_) => panic!("Non-refundable transfer to a legacy account."), + Account::Account(account) => { + account.set_nonrefundable( + account.nonrefundable().checked_add(deposit).ok_or_else(|| { + StorageError::StorageInconsistentState( + "Non-refundable account balance integer overflow".to_string(), + ) + })?, + ); + } + } } else { account.set_amount(account.amount().checked_add(deposit).ok_or_else(|| { StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) From b9dddbebdae6a6f8d8a820203e6b9f576aa2c9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Wed, 10 Jan 2024 15:12:59 +0100 Subject: [PATCH 23/36] Add comements --- chain/client/src/test_utils/test_env.rs | 1 + core/primitives-core/src/account.rs | 31 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/chain/client/src/test_utils/test_env.rs b/chain/client/src/test_utils/test_env.rs index 9af3d0bfc77..9b231981056 100644 --- a/chain/client/src/test_utils/test_env.rs +++ b/chain/client/src/test_utils/test_env.rs @@ -380,6 +380,7 @@ impl TestEnv { } } + /// Passes the given query to the runtime adapter using the current head and returns a result. pub fn query_view(&mut self, request: QueryRequest) -> Result { let head = self.clients[0].chain.head().unwrap(); let head_block = self.clients[0].chain.get_block(&head.last_block_hash).unwrap(); diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 71c0eccd7c8..0b0b47c825a 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -39,6 +39,7 @@ impl TryFrom for AccountVersion { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +/// A standard, default account format that uses versioning. #[derive(serde::Serialize, PartialEq, Eq, Debug, Clone)] pub struct StandardAccount { /// The total not locked, refundable tokens. @@ -82,6 +83,9 @@ impl StandardAccount { } } +/// Special account format used for backward compatibility reasons. +/// This is the account format we parse from mainnet genesis file, +/// so it should not be changed, as it would change the genesis hash. #[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] #[cfg_attr( not(feature = "protocol_feature_nonrefundable_transfer_nep491"), @@ -105,7 +109,7 @@ pub enum Account { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Account { - /// The total not locked, refundable tokens. + /// The total not locked tokens. #[serde(with = "dec_format")] amount: Balance, /// The amount locked due to staking. @@ -286,6 +290,7 @@ impl Account { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +/// We do not serialize Account enum type discriminator, to be consistent with the way we deserialize. impl serde::Serialize for Account { fn serialize(&self, serializer: S) -> Result where @@ -298,6 +303,8 @@ impl serde::Serialize for Account { } } +/// Legacy accounts (e.g. accounts that we parse from the mainnet genesis file) +/// do not have the `nonrefundable` field. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] impl<'de> serde::Deserialize<'de> for Account { fn deserialize(deserializer: D) -> Result @@ -312,8 +319,10 @@ impl<'de> serde::Deserialize<'de> for Account { locked: Balance, code_hash: CryptoHash, storage_usage: StorageUsage, + // If the field is missing, serde will use None as the default. #[serde(default, with = "dec_format")] nonrefundable: Option, + version: Option, } let account_data = AccountData::deserialize(deserializer)?; @@ -324,8 +333,9 @@ impl<'de> serde::Deserialize<'de> for Account { locked: account_data.locked, code_hash: account_data.code_hash, storage_usage: account_data.storage_usage, - nonrefundable: nonrefundable, - version: AccountVersion::V2, + nonrefundable, + // The `version` field must be present in non-legacy serde serialized accounts. + version: account_data.version.expect("Missing `version` field"), })), None => Ok(Account::LegacyAccount(LegacyAccount { amount: account_data.amount, @@ -343,13 +353,13 @@ impl BorshDeserialize for Account { // either a sentinel or a balance. let sentinel_or_amount = u128::deserialize_reader(rd)?; if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { - // Account v2 or newer + // Account v2 or newer. let version_byte = u8::deserialize_reader(rd)?; - if version_byte != 2 { + if version_byte < 2 { return Err(io::Error::new( io::ErrorKind::InvalidData, format!( - "Error deserializing account: version {} does not equal 2", + "Error deserializing account: version {} less than 2", version_byte ), )); @@ -374,6 +384,9 @@ impl BorshDeserialize for Account { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] { + // NOTE(staffik): This part of the code is unreachable, since the serialization sentinel + // could only be read from the serialized state since the Non-refundable transfer feature landed. + // Kept for compilation reason, would be removed after stabilization. Ok(Account { amount, locked, code_hash, storage_usage, version }) } @@ -434,10 +447,10 @@ impl BorshSerialize for Account { { match self { Account::LegacyAccount(_) => legacy_account.serialize(writer), - // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 - // while serializing. But that would break the borsh assumptions - // of unique binary representation. Account::Account(account) => match account.version() { + // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 + // while serializing. But that would break the borsh assumptions + // of unique binary representation. AccountVersion::V1 => legacy_account.serialize(writer), // Note(jakmeier): These accounts are serialized in merklized state. // I would really like to avoid migration of the MPT. From 9cfaa0016cbf35c03f60ae4bae273c895dfc1f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Wed, 10 Jan 2024 20:32:54 +0100 Subject: [PATCH 24/36] No need to introduce Account variants --- core/primitives-core/src/account.rs | 534 ++++++++++++---------------- runtime/runtime/src/actions.rs | 21 +- 2 files changed, 241 insertions(+), 314 deletions(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 0b0b47c825a..b288b4ba7e9 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -9,6 +9,7 @@ use std::io; BorshSerialize, BorshDeserialize, PartialEq, + PartialOrd, Eq, Clone, Copy, @@ -38,10 +39,13 @@ impl TryFrom for AccountVersion { } } -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -/// A standard, default account format that uses versioning. +/// Per account information stored in the state. +#[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + derive(serde::Deserialize) +)] #[derive(serde::Serialize, PartialEq, Eq, Debug, Clone)] -pub struct StandardAccount { +pub struct Account { /// The total not locked, refundable tokens. #[serde(with = "dec_format")] amount: Balance, @@ -49,6 +53,7 @@ pub struct StandardAccount { #[serde(with = "dec_format")] locked: Balance, /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] #[serde(with = "dec_format")] nonrefundable: Balance, /// Hash of the code stored in the storage for this account. @@ -56,70 +61,6 @@ pub struct StandardAccount { /// Storage used by the given account, includes account id, this struct, access keys and other data. storage_usage: StorageUsage, /// Version of Account in re migrations and similar - #[serde(default)] - version: AccountVersion, -} - -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -impl StandardAccount { - #[inline] - pub fn nonrefundable(&self) -> Balance { - self.nonrefundable - } - - #[inline] - pub fn version(&self) -> AccountVersion { - self.version - } - - #[inline] - pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { - self.nonrefundable = nonrefundable; - } - - #[inline] - pub fn set_version(&mut self, version: AccountVersion) { - self.version = version; - } -} - -/// Special account format used for backward compatibility reasons. -/// This is the account format we parse from mainnet genesis file, -/// so it should not be changed, as it would change the genesis hash. -#[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] -#[cfg_attr( - not(feature = "protocol_feature_nonrefundable_transfer_nep491"), - derive(BorshDeserialize) -)] -pub struct LegacyAccount { - amount: Balance, - locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, -} - -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -/// Per account information stored in the state. -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum Account { - LegacyAccount(LegacyAccount), - Account(StandardAccount), -} - -#[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] -#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct Account { - /// The total not locked tokens. - #[serde(with = "dec_format")] - amount: Balance, - /// The amount locked due to staking. - #[serde(with = "dec_format")] - locked: Balance, - /// Hash of the code stored in the storage for this account. - code_hash: CryptoHash, - /// Storage used by the given account, includes account id, this struct, access keys and other data. - storage_usage: StorageUsage, - /// Version of Account in re migrations and similar /// /// Note(jakmeier): Why does this exist? We only have one version right now /// and the code doesn't allow adding a new version at all since this field @@ -145,43 +86,27 @@ impl Account { storage_usage: StorageUsage, ) -> Self { #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - assert_eq!(nonrefundable, 0); - Account { amount, locked, code_hash, storage_usage, version: AccountVersion::default() } - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Self::Account(StandardAccount { + assert_eq!(nonrefundable, 0); + Account { amount, locked, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, code_hash, storage_usage, version: AccountVersion::default(), - }) + } } #[inline] pub fn amount(&self) -> Balance { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.amount - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.amount, - Account::Account(account) => account.amount, - } + self.amount } #[inline] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn nonrefundable(&self) -> Balance { - match self { - Account::LegacyAccount(_) => 0, - Account::Account(account) => account.nonrefundable(), - } + self.nonrefundable } #[inline] @@ -192,119 +117,73 @@ impl Account { #[inline] pub fn locked(&self) -> Balance { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.locked - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.locked, - Account::Account(account) => account.locked, - } + self.locked } #[inline] pub fn code_hash(&self) -> CryptoHash { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.code_hash - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.code_hash, - Account::Account(account) => account.code_hash, - } + self.code_hash } #[inline] pub fn storage_usage(&self) -> StorageUsage { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.storage_usage - } + self.storage_usage + } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.storage_usage, - Account::Account(account) => account.storage_usage, - } + #[inline] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + pub fn version(&self) -> AccountVersion { + self.version } #[inline] pub fn set_amount(&mut self, amount: Balance) { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.amount = amount; - } + self.amount = amount; + } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.amount = amount, - Account::Account(account) => account.amount = amount, - } + #[inline] + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { + self.nonrefundable = nonrefundable; } #[inline] pub fn set_locked(&mut self, locked: Balance) { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.locked = locked; - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.locked = locked, - Account::Account(account) => account.locked = locked, - } + self.locked = locked; } #[inline] pub fn set_code_hash(&mut self, code_hash: CryptoHash) { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.code_hash = code_hash; - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.code_hash = code_hash, - Account::Account(account) => account.code_hash = code_hash, - } + self.code_hash = code_hash; } #[inline] pub fn set_storage_usage(&mut self, storage_usage: StorageUsage) { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - self.storage_usage = storage_usage; - } + self.storage_usage = storage_usage; + } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match self { - Account::LegacyAccount(account) => account.storage_usage = storage_usage, - Account::Account(account) => account.storage_usage = storage_usage, - } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + pub fn set_version(&mut self, version: AccountVersion) { + self.version = version; } } -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -/// We do not serialize Account enum type discriminator, to be consistent with the way we deserialize. -impl serde::Serialize for Account { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Account::LegacyAccount(account) => serde::Serialize::serialize(account, serializer), - Account::Account(account) => serde::Serialize::serialize(account, serializer), - } - } +/// Note(jakmeier): Even though this is called "legacy", it looks like this is +/// the one and only serialization format of Accounts currently in use. +#[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] +#[cfg_attr( + not(feature = "protocol_feature_nonrefundable_transfer_nep491"), + derive(BorshDeserialize) +)] +struct LegacyAccount { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, } /// Legacy accounts (e.g. accounts that we parse from the mainnet genesis file) -/// do not have the `nonrefundable` field. +/// do not have the `nonrefundable` field. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] impl<'de> serde::Deserialize<'de> for Account { fn deserialize(deserializer: D) -> Result @@ -317,32 +196,51 @@ impl<'de> serde::Deserialize<'de> for Account { amount: Balance, #[serde(with = "dec_format")] locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, // If the field is missing, serde will use None as the default. #[serde(default, with = "dec_format")] nonrefundable: Option, + code_hash: CryptoHash, + storage_usage: StorageUsage, + #[serde(default)] version: Option, } let account_data = AccountData::deserialize(deserializer)?; match account_data.nonrefundable { - Some(nonrefundable) => Ok(Account::Account(StandardAccount { - amount: account_data.amount, - locked: account_data.locked, - code_hash: account_data.code_hash, - storage_usage: account_data.storage_usage, - nonrefundable, - // The `version` field must be present in non-legacy serde serialized accounts. - version: account_data.version.expect("Missing `version` field"), - })), - None => Ok(Account::LegacyAccount(LegacyAccount { + Some(nonrefundable) => { + // Given that the `nonrefundable` field has been serialized, the `version` field must has been serialized too. + let version = match account_data.version { + Some(version) => version, + None => { + return Err(serde::de::Error::custom("Missing `version` field")); + } + }; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + if version < AccountVersion::V2 && nonrefundable > 0 { + return Err(serde::de::Error::custom( + "Non-refundable positive amount exists for account version older than V2", + )); + } + + Ok(Account { + amount: account_data.amount, + locked: account_data.locked, + code_hash: account_data.code_hash, + storage_usage: account_data.storage_usage, + nonrefundable, + version, + }) + } + None => Ok(Account { amount: account_data.amount, locked: account_data.locked, code_hash: account_data.code_hash, storage_usage: account_data.storage_usage, - })), + nonrefundable: 0, + version: AccountVersion::V1, + }), } } } @@ -355,16 +253,6 @@ impl BorshDeserialize for Account { if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { // Account v2 or newer. let version_byte = u8::deserialize_reader(rd)?; - if version_byte < 2 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "Error deserializing account: version {} less than 2", - version_byte - ), - )); - } - let version = AccountVersion::try_from(version_byte).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, @@ -374,6 +262,13 @@ impl BorshDeserialize for Account { ), ) })?; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + if version < AccountVersion::V2 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Expected account version 2 or higher, got {:?}", version), + )); + } let amount = u128::deserialize_reader(rd)?; let locked = u128::deserialize_reader(rd)?; @@ -382,49 +277,30 @@ impl BorshDeserialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let nonrefundable = u128::deserialize_reader(rd)?; - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - // NOTE(staffik): This part of the code is unreachable, since the serialization sentinel - // could only be read from the serialized state since the Non-refundable transfer feature landed. - // Kept for compilation reason, would be removed after stabilization. - Ok(Account { amount, locked, code_hash, storage_usage, version }) - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Ok(Account::Account(StandardAccount { + Ok(Account { amount, locked, code_hash, storage_usage, version, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable, - })) + }) } else { // Account v1 let locked = u128::deserialize_reader(rd)?; let code_hash = CryptoHash::deserialize_reader(rd)?; let storage_usage = StorageUsage::deserialize_reader(rd)?; - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - Ok(Account { - amount: sentinel_or_amount, - locked, - code_hash, - storage_usage, - version: AccountVersion::V1, - }) - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Ok(Account::Account(StandardAccount { + Ok(Account { amount: sentinel_or_amount, locked, code_hash, storage_usage, version: AccountVersion::V1, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] nonrefundable: 0, - })) + }) } } } @@ -445,43 +321,39 @@ impl BorshSerialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] { - match self { - Account::LegacyAccount(_) => legacy_account.serialize(writer), - Account::Account(account) => match account.version() { - // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 - // while serializing. But that would break the borsh assumptions - // of unique binary representation. - AccountVersion::V1 => legacy_account.serialize(writer), - // Note(jakmeier): These accounts are serialized in merklized state. - // I would really like to avoid migration of the MPT. - // This here would keep old accounts in the old format - // and only allow nonrefundable storage on new accounts. - AccountVersion::V2 => { - #[derive(BorshSerialize)] - struct AccountV2 { - amount: Balance, - locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, - nonrefundable: Balance, - } - - let account = AccountV2 { - amount: self.amount(), - locked: self.locked(), - code_hash: self.code_hash(), - storage_usage: self.storage_usage(), - nonrefundable: self.nonrefundable(), - }; - let sentinel = Account::SERIALIZATION_SENTINEL; - // For now a constant, but if we need V3 later we can use this - // field instead of sentinel magic. - let version = 2u8; - BorshSerialize::serialize(&sentinel, writer)?; - BorshSerialize::serialize(&version, writer)?; - account.serialize(writer) + match self.version { + // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 + // while serializing. But that would break the borsh assumptions + // of unique binary representation. + AccountVersion::V1 => legacy_account.serialize(writer), + // Note(jakmeier): These accounts are serialized in merklized state. + // I would really like to avoid migration of the MPT. + // This here would keep old accounts in the old format + // and only allow nonrefundable storage on new accounts. + AccountVersion::V2 => { + #[derive(BorshSerialize)] + struct AccountV2 { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, + nonrefundable: Balance, } - }, + let account = AccountV2 { + amount: self.amount(), + locked: self.locked(), + code_hash: self.code_hash(), + storage_usage: self.storage_usage(), + nonrefundable: self.nonrefundable(), + }; + let sentinel = Account::SERIALIZATION_SENTINEL; + // For now a constant, but if we need V3 later we can use this + // field instead of sentinel magic. + let version = 2u8; + BorshSerialize::serialize(&sentinel, writer)?; + BorshSerialize::serialize(&version, writer)?; + account.serialize(writer) + } } } } @@ -593,30 +465,15 @@ mod tests { code_hash: CryptoHash::default(), storage_usage: 100, }; - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let old_account = Account::LegacyAccount(old_account); let serialized_account = serde_json::to_string(&old_account).unwrap(); let new_account: Account = serde_json::from_str(&serialized_account).unwrap(); - - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - assert_eq!(new_account.amount(), old_account.amount); - assert_eq!(new_account.locked(), old_account.locked); - assert_eq!(new_account.code_hash(), old_account.code_hash); - assert_eq!(new_account.storage_usage(), old_account.storage_usage); - assert_eq!(new_account.version, AccountVersion::V1); - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match new_account.clone() { - Account::Account(_) => { - panic!("Expected LegacyAccount, but found StandardAccount") - } - Account::LegacyAccount(account) => { - assert_eq!(account, account); - } - } + assert_eq!(new_account.amount(), old_account.amount); + assert_eq!(new_account.locked(), old_account.locked); + assert_eq!(new_account.code_hash(), old_account.code_hash); + assert_eq!(new_account.storage_usage(), old_account.storage_usage); + assert_eq!(new_account.nonrefundable(), 0); + assert_eq!(new_account.version, AccountVersion::V1); let new_serialized_account = serde_json::to_string(&new_account).unwrap(); let deserialized_account: Account = serde_json::from_str(&new_serialized_account).unwrap(); @@ -631,30 +488,14 @@ mod tests { code_hash: CryptoHash::default(), storage_usage: 300, }; - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let old_account = Account::LegacyAccount(old_account); - let mut old_bytes = &borsh::to_vec(&old_account).unwrap()[..]; let new_account = ::deserialize(&mut old_bytes).unwrap(); - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - { - assert_eq!(new_account.amount(), old_account.amount); - assert_eq!(new_account.locked(), old_account.locked); - assert_eq!(new_account.code_hash(), old_account.code_hash); - assert_eq!(new_account.storage_usage(), old_account.storage_usage); - assert_eq!(new_account.version, AccountVersion::V1); - } - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match new_account { - Account::Account(ref acount) => { - assert_eq!(acount.version, AccountVersion::V1); - } - Account::LegacyAccount(_) => { - panic!("Expected StandardAccount, but found LegacyAccount") - } - } + assert_eq!(new_account.amount(), old_account.amount); + assert_eq!(new_account.locked(), old_account.locked); + assert_eq!(new_account.code_hash(), old_account.code_hash); + assert_eq!(new_account.storage_usage(), old_account.storage_usage); + assert_eq!(new_account.version, AccountVersion::V1); let mut new_bytes = &borsh::to_vec(&new_account).unwrap()[..]; let deserialized_account = @@ -663,19 +504,112 @@ mod tests { } #[test] - fn test_account_serde_serialization() { - let account = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); + fn test_account_v1_serde_serialization() { + let account = Account { + amount: 10_000_000, + locked: 100_000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, + code_hash: CryptoHash::default(), + storage_usage: 1000, + version: AccountVersion::V1, + }; let serialized_account = serde_json::to_string(&account).unwrap(); let deserialized_account: Account = serde_json::from_str(&serialized_account).unwrap(); assert_eq!(deserialized_account, account); } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + /// Serialization of account V1 with non-refundable amount greater than 0 would pass without an error, + /// but an error would be raised on deserialization of such invalid data. + #[test] + fn test_account_v1_serde_serialization_invalid_data() { + let account = Account { + amount: 10_000_000, + locked: 100_000, + nonrefundable: 1, + code_hash: CryptoHash::default(), + storage_usage: 1000, + version: AccountVersion::V1, + }; + let serialized_account = serde_json::to_string(&account).unwrap(); + let deserialization_result: Result = + serde_json::from_str(&serialized_account); + assert!(deserialization_result.is_err()); + } + #[test] - fn test_account_borsh_serialization() { - let account = Account::new(1_000_000, 1_000_000, 0, CryptoHash::default(), 100); + fn test_account_v1_borsh_serialization() { + let account = Account { + amount: 1_000_000, + locked: 1_000_000, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: 0, + code_hash: CryptoHash::default(), + storage_usage: 100, + version: AccountVersion::V1, + }; + let serialized_account = borsh::to_vec(&account).unwrap(); + assert_eq!( + &hash(&serialized_account).to_string(), + "EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ" + ); + let deserialized_account = + ::deserialize(&mut &serialized_account[..]).unwrap(); + assert_eq!(deserialized_account, account); + } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + /// That must not happen, but if a V1 account had nonrefundable amount greater than zero, + /// it would be truncated during Borsh serialization. + #[test] + fn test_account_v1_borsh_serialization_invalid_data() { + let account = Account { + amount: 1_000_000, + locked: 1_000_000, + nonrefundable: 1, + code_hash: CryptoHash::default(), + storage_usage: 100, + version: AccountVersion::V1, + }; + let serialized_account = borsh::to_vec(&account).unwrap(); + assert_eq!( + &hash(&serialized_account).to_string(), + "EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ" + ); + let deserialized_account = + ::deserialize(&mut &serialized_account[..]).unwrap(); + assert_eq!(deserialized_account.nonrefundable, 0); + } + + #[test] + fn test_account_v2_serde_serialization() { + let account = Account { + amount: 10_000_000, + locked: 100_000, + nonrefundable: 37, + code_hash: CryptoHash::default(), + storage_usage: 1000, + version: AccountVersion::V2, + }; + let serialized_account = serde_json::to_string(&account).unwrap(); + let deserialized_account: Account = serde_json::from_str(&serialized_account).unwrap(); + assert_eq!(deserialized_account, account); + } + + #[test] + fn test_account_v2_borsh_serialization() { + let account = Account { + amount: 1_000_000, + locked: 1_000_000, + nonrefundable: 42, + code_hash: CryptoHash::default(), + storage_usage: 100, + version: AccountVersion::V2, + }; let serialized_account = borsh::to_vec(&account).unwrap(); if cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491") { - expect_test::expect!("HaZPNG4KpXQ9Mre4PAA83V5usqXsA4zy4vMwSXBiBcQv") + expect_test::expect!("A3Ypkhkm6G5PYwHZw1eKYVunEzafLu8fbTAYLGts2AGy") } else { expect_test::expect!("EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ") } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 70ee119c3b9..a687ca55e0e 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -405,20 +405,13 @@ pub(crate) fn action_transfer( if nonrefundable { assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - match account { - // This path cannot happen. It is rejected by `check_account_existence` with - // an error `NonRefundableBalanceToExistingAccount` as legacy account must already exist. - Account::LegacyAccount(_) => panic!("Non-refundable transfer to a legacy account."), - Account::Account(account) => { - account.set_nonrefundable( - account.nonrefundable().checked_add(deposit).ok_or_else(|| { - StorageError::StorageInconsistentState( - "Non-refundable account balance integer overflow".to_string(), - ) - })?, - ); - } - } + account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else( + || { + StorageError::StorageInconsistentState( + "Non-refundable account balance integer overflow".to_string(), + ) + }, + )?); } else { account.set_amount(account.amount().checked_add(deposit).ok_or_else(|| { StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) From 9ffe5ad532b79d580663ca9b83569f48ec802e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Wed, 10 Jan 2024 21:06:48 +0100 Subject: [PATCH 25/36] Rename ReserveStorage --> NonrefundableStorageTransfer --- chain/rosetta-rpc/src/adapters/mod.rs | 4 ++-- core/primitives-core/src/account.rs | 8 ++++---- core/primitives/src/action/mod.rs | 6 +++--- core/primitives/src/transaction.rs | 2 +- core/primitives/src/views.rs | 12 ++++++------ .../tests/client/features/nonrefundable_transfer.rs | 8 +++++--- runtime/runtime/src/actions.rs | 4 ++-- runtime/runtime/src/config.rs | 6 ++---- runtime/runtime/src/lib.rs | 6 ++++-- runtime/runtime/src/verifier.rs | 2 +- tools/state-viewer/src/contract_accounts.rs | 6 ++++-- 11 files changed, 34 insertions(+), 30 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 727b970e212..109b03cd389 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -354,8 +354,8 @@ impl From for Vec { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - // Note: Both refundable and non-refundable transfers are considered as available balance. - near_primitives::transaction::Action::ReserveStorage(action) => { + // Note(jakmeier): Both refundable and non-refundable transfers are considered as available balance. + near_primitives::transaction::Action::NonrefundableStorageTransfer(action) => { let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); let sender_transfer_operation_id = diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index b288b4ba7e9..6f0ee7530f7 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -131,7 +131,6 @@ impl Account { } #[inline] - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn version(&self) -> AccountVersion { self.version } @@ -162,7 +161,6 @@ impl Account { self.storage_usage = storage_usage; } - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] pub fn set_version(&mut self, version: AccountVersion) { self.version = version; } @@ -182,8 +180,8 @@ struct LegacyAccount { storage_usage: StorageUsage, } -/// Legacy accounts (e.g. accounts that we parse from the mainnet genesis file) -/// do not have the `nonrefundable` field. +/// We need custom serde deserialization in order to parse mainnet genesis accounts (LegacyAccounts) +/// as accounts V1. This preserves the mainnet genesis hash. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] impl<'de> serde::Deserialize<'de> for Account { fn deserialize(deserializer: D) -> Result @@ -582,6 +580,7 @@ mod tests { assert_eq!(deserialized_account.nonrefundable, 0); } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] #[test] fn test_account_v2_serde_serialization() { let account = Account { @@ -597,6 +596,7 @@ mod tests { assert_eq!(deserialized_account, account); } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] #[test] fn test_account_v2_borsh_serialization() { let account = Account { diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index ae3eb64aaed..bbd6a7dcbe3 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -163,7 +163,7 @@ pub struct TransferAction { serde::Deserialize, )] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -pub struct ReserveStorageAction { +pub struct NonrefundableStorageTransferAction { #[serde(with = "dec_format")] pub deposit: Balance, } @@ -194,7 +194,7 @@ pub enum Action { DeleteAccount(DeleteAccountAction), Delegate(Box), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ReserveStorage(ReserveStorageAction), + NonrefundableStorageTransfer(NonrefundableStorageTransferAction), } const _: () = assert!( cfg!(not(target_pointer_width = "64")) || std::mem::size_of::() == 32, @@ -213,7 +213,7 @@ impl Action { Action::FunctionCall(a) => a.deposit, Action::Transfer(a) => a.deposit, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(a) => a.deposit, + Action::NonrefundableStorageTransfer(a) => a.deposit, _ => 0, } } diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index c332edb7757..ca4060b01fd 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -15,7 +15,7 @@ use std::fmt; use std::hash::{Hash, Hasher}; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -pub use crate::action::ReserveStorageAction; +pub use crate::action::NonrefundableStorageTransferAction; pub use crate::action::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index d925f3853b0..5851f6b5db0 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -24,7 +24,7 @@ use crate::sharding::{ ShardChunkHeaderV3, }; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -use crate::transaction::ReserveStorageAction; +use crate::transaction::NonrefundableStorageTransferAction; use crate::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithIdAndProof, @@ -1198,7 +1198,7 @@ pub enum ActionView { deposit: Balance, }, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ReserveStorage { + NonrefundableStorageTransfer { #[serde(with = "dec_format")] deposit: Balance, }, @@ -1239,8 +1239,8 @@ impl From for ActionView { }, Action::Transfer(action) => ActionView::Transfer { deposit: action.deposit }, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(action) => { - ActionView::ReserveStorage { deposit: action.deposit } + Action::NonrefundableStorageTransfer(action) => { + ActionView::NonrefundableStorageTransfer { deposit: action.deposit } } Action::Stake(action) => { ActionView::Stake { stake: action.stake, public_key: action.public_key } @@ -1280,8 +1280,8 @@ impl TryFrom for Action { } ActionView::Transfer { deposit } => Action::Transfer(TransferAction { deposit }), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ActionView::ReserveStorage { deposit } => { - Action::ReserveStorage(ReserveStorageAction { deposit }) + ActionView::NonrefundableStorageTransfer { deposit } => { + Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { deposit }) } ActionView::Stake { stake, public_key } => { Action::Stake(Box::new(StakeAction { stake, public_key })) diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index b11215d6cf9..4797796a46e 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -2,7 +2,7 @@ //! accounts storage staking balance without that someone being able to run off //! with the money. //! -//! This feature introduces the ReserveStorage action. +//! This feature introduces the NonrefundableStorageTransfer action. //! //! NEP: https://github.com/near/NEPs/pull/491 @@ -16,7 +16,7 @@ use near_primitives::errors::{ use near_primitives::test_utils::{eth_implicit_test_account, near_implicit_test_account}; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, - ReserveStorageAction, SignedTransaction, TransferAction, + NonrefundableStorageTransferAction, SignedTransaction, TransferAction, }; use near_primitives::types::{AccountId, Balance}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; @@ -133,7 +133,9 @@ fn exec_transfer( } if nonrefundable { - actions.push(Action::ReserveStorage(ReserveStorageAction { deposit })); + actions.push(Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { + deposit, + })); } else { actions.push(Action::Transfer(TransferAction { deposit })); } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index a687ca55e0e..c51bfca64e5 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -955,7 +955,7 @@ pub(crate) fn check_actor_permissions( Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), Action::Delegate(_) => (), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(_) => (), + Action::NonrefundableStorageTransfer(_) => (), }; Ok(()) } @@ -1012,7 +1012,7 @@ pub(crate) fn check_account_existence( } } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(_) => { + Action::NonrefundableStorageTransfer(_) => { if account.is_none() { return check_transfer_to_nonexisting_account( config, diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 60d8867f73b..8b1e3c72278 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -103,8 +103,7 @@ pub fn total_send_fees( ) } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ReserveStorage(_) => { - // TODO(nonrefundable-storage) When stabilizing, merge with branch above + NonrefundableStorageTransfer(_) => { // Account for implicit account creation transfer_send_fee( fees, @@ -210,8 +209,7 @@ pub fn exec_fee(config: &RuntimeConfig, action: &Action, receiver_id: &AccountId ) } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ReserveStorage(_) => { - // TODO(nonrefundable-storage) When stabilizing, merge with branch above + NonrefundableStorageTransfer(_) => { // Account for implicit account creation transfer_exec_fee( fees, diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 09431a21d61..2c388a739dd 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -29,7 +29,7 @@ use near_primitives::transaction::{ SignedTransaction, TransferAction, }; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -use near_primitives::transaction::{DeleteAccountAction, ReserveStorageAction}; +use near_primitives::transaction::{DeleteAccountAction, NonrefundableStorageTransferAction}; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas, @@ -383,7 +383,9 @@ impl Runtime { )?; } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(ReserveStorageAction { deposit }) => { + Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { + deposit, + }) => { action_transfer_or_implicit_account_creation( account, *deposit, diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index e39cf7907ed..2acbde348ad 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -400,7 +400,7 @@ pub fn validate_action( Action::FunctionCall(a) => validate_function_call_action(limit_config, a), Action::Transfer(_) => Ok(()), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - Action::ReserveStorage(_) => { + Action::NonrefundableStorageTransfer(_) => { check_feature_enabled(ProtocolFeature::NonRefundableBalance, current_protocol_version) } Action::Stake(a) => validate_stake_action(a), diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index ee939c79436..13b3bc0939d 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -129,7 +129,7 @@ pub(crate) enum ActionType { FunctionCall, Transfer, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ReserveStorage, + NonrefundableStorageTransfer, Stake, AddKey, DeleteKey, @@ -338,7 +338,9 @@ fn try_find_actions_spawned_by_receipt( #[cfg( feature = "protocol_feature_nonrefundable_transfer_nep491" )] - Action::ReserveStorage(_) => ActionType::ReserveStorage, + Action::NonrefundableStorageTransfer(_) => { + ActionType::NonrefundableStorageTransfer + } Action::Stake(_) => ActionType::Stake, Action::AddKey(_) => ActionType::AddKey, Action::DeleteKey(_) => ActionType::DeleteKey, From 65b01757e8f701092cd70313448ccf15a7f2bf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Fri, 12 Jan 2024 11:23:00 +0100 Subject: [PATCH 26/36] Minor refactors, update test --- .../client/features/nonrefundable_transfer.rs | 185 +++++++++++++----- .../res/wallet_contract.wasm | Bin 93368 -> 94343 bytes runtime/runtime/src/actions.rs | 31 ++- runtime/runtime/src/lib.rs | 8 +- 4 files changed, 161 insertions(+), 63 deletions(-) diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 4797796a46e..3f6df8af353 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -27,6 +27,9 @@ use near_primitives_core::account::{AccessKey, AccessKeyPermission}; use nearcore::config::GenesisExt; use nearcore::test_utils::TestEnvNightshadeSetupExt; use nearcore::NEAR_BASE; +use testlib::fees_utils::FeeHelper; + +use crate::node::RuntimeNode; /// Default sender to use in tests of this module. fn sender() -> AccountId { @@ -60,6 +63,11 @@ fn setup_env() -> TestEnv { setup_env_with_protocol_version(None) } +fn fee_helper() -> FeeHelper { + let node = RuntimeNode::new(&sender()); + crate::tests::standard_cases::fee_helper(&node) +} + fn get_nonce(env: &mut TestEnv, signer: &InMemorySigner) -> u64 { let request = QueryRequest::ViewAccessKey { account_id: signer.account_id.clone(), @@ -100,6 +108,13 @@ fn execute_transaction_from_actions( tx_result } +struct TransferConfig { + nonrefundable: bool, + account_creation: bool, + implicit_account_creation: bool, + deploy_contract: bool, +} + /// Submits a transfer (either regular or non-refundable). /// /// This methods checks that the balance is subtracted from the sender and added @@ -109,13 +124,10 @@ fn exec_transfer( signer: InMemorySigner, receiver: AccountId, deposit: Balance, - nonrefundable: bool, - account_creation: bool, - implicit_account_creation: bool, - deploy_contract: bool, + config: TransferConfig, ) -> Result { let sender_pre_balance = env.query_balance(sender()); - let (receiver_before_amount, receiver_before_nonrefundable) = if account_creation { + let (receiver_before_amount, receiver_before_nonrefundable) = if config.account_creation { (0, 0) } else { let receiver_before = env.query_account(receiver.clone()); @@ -124,7 +136,7 @@ fn exec_transfer( let mut actions = vec![]; - if account_creation && !implicit_account_creation { + if config.account_creation && !config.implicit_account_creation { actions.push(Action::CreateAccount(CreateAccountAction {})); actions.push(Action::AddKey(Box::new(AddKeyAction { public_key: PublicKey::from_seed(KeyType::ED25519, receiver.as_str()), @@ -132,7 +144,7 @@ fn exec_transfer( }))); } - if nonrefundable { + if config.nonrefundable { actions.push(Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { deposit, })); @@ -140,7 +152,7 @@ fn exec_transfer( actions.push(Action::Transfer(TransferAction { deposit })); } - if deploy_contract { + if config.deploy_contract { let contract = near_test_contracts::sized_contract(1500 as usize); actions.push(Action::DeployContract(DeployContractAction { code: contract.to_vec() })) } @@ -162,12 +174,12 @@ fn exec_transfer( assert_eq!(sender_pre_balance - deposit - gas_cost, env.query_balance(sender())); let receiver_after = env.query_account(receiver); - let (receiver_expected_amount_after, receiver_expected_non_refundable_after) = if nonrefundable - { - (receiver_before_amount, receiver_before_nonrefundable + deposit) - } else { - (receiver_before_amount + deposit, receiver_before_nonrefundable) - }; + let (receiver_expected_amount_after, receiver_expected_non_refundable_after) = + if config.nonrefundable { + (receiver_before_amount, receiver_before_nonrefundable + deposit) + } else { + (receiver_before_amount + deposit, receiver_before_nonrefundable) + }; assert_eq!(receiver_after.amount, receiver_expected_amount_after); assert_eq!(receiver_after.nonrefundable, receiver_expected_non_refundable_after); @@ -178,8 +190,9 @@ fn exec_transfer( fn delete_account( env: &mut TestEnv, signer: &InMemorySigner, + beneficiary_id: AccountId, ) -> Result { - let actions = vec![Action::DeleteAccount(DeleteAccountAction { beneficiary_id: receiver() })]; + let actions = vec![Action::DeleteAccount(DeleteAccountAction { beneficiary_id })]; execute_transaction_from_actions(env, actions, &signer, signer.account_id.clone()) } @@ -193,37 +206,54 @@ fn deleting_account_with_non_refundable_storage() { KeyType::ED25519, new_account_id.as_str(), ); + let nonrefundable_deposit = NEAR_BASE; // Create account with non-refundable storage. // Deploy a contract that does not fit within Zero-balance account limit. let create_account_tx_result = exec_transfer( &mut env, signer(), new_account_id.clone(), - NEAR_BASE, - true, - true, - false, - true, + nonrefundable_deposit, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: false, + deploy_contract: true, + }, ); create_account_tx_result.unwrap().assert_success(); // Send some NEAR (refundable) so that the new account is able to pay the gas for its deletion in the next transaction. + let deposit = 10u128.pow(20); let send_money_tx_result = exec_transfer( &mut env, signer(), new_account_id.clone(), - 10u128.pow(20), - false, - false, - false, - false, + deposit, + TransferConfig { + nonrefundable: false, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, ); send_money_tx_result.unwrap().assert_success(); // Delete the new account (that has 1 NEAR of non-refundable balance). - let delete_account_tx_result = delete_account(&mut env, &new_account); + let beneficiary_id = receiver(); + let beneficiary_before = env.query_account(beneficiary_id.clone()); + let delete_account_tx_result = delete_account(&mut env, &new_account, beneficiary_id.clone()); delete_account_tx_result.unwrap().assert_success(); assert!(!account_exists(&mut env, new_account_id)); + + // Check that the beneficiary account received the remaining balance from the deleted account, + // but none of the non-refundable balance. + let beneficiary_after = env.query_account(beneficiary_id); + assert_eq!( + beneficiary_after.amount, + beneficiary_before.amount + deposit - fee_helper().prepaid_delete_account_cost() + ); + assert_eq!(beneficiary_after.nonrefundable, beneficiary_before.nonrefundable); } /// Non-refundable balance cannot be transferred. @@ -242,10 +272,12 @@ fn non_refundable_balance_cannot_be_transferred() { signer(), new_account_id.clone(), NEAR_BASE, - true, - true, - false, - false, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: false, + deploy_contract: false, + }, ); create_account_tx_result.unwrap().assert_success(); @@ -256,10 +288,12 @@ fn non_refundable_balance_cannot_be_transferred() { new_account.clone(), receiver(), 1, - nonrefundable, - false, - false, - false, + TransferConfig { + nonrefundable, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, ); match transfer_tx_result { Err(InvalidTxError::NotEnoughBalance { signer_id, balance, .. }) => { @@ -276,8 +310,18 @@ fn non_refundable_balance_cannot_be_transferred() { fn non_refundable_balance_allows_1kb_state_with_zero_balance() { let mut env = setup_env(); let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); - let tx_result = - exec_transfer(&mut env, signer(), new_account_id, NEAR_BASE / 5, true, true, false, true); + let tx_result = exec_transfer( + &mut env, + signer(), + new_account_id, + NEAR_BASE / 5, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: false, + deploy_contract: true, + }, + ); tx_result.unwrap().assert_success(); } @@ -285,8 +329,18 @@ fn non_refundable_balance_allows_1kb_state_with_zero_balance() { #[test] fn non_refundable_transfer_create_named_account() { let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); - let tx_result = - exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, false, false); + let tx_result = exec_transfer( + &mut setup_env(), + signer(), + new_account_id, + 1, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: false, + deploy_contract: false, + }, + ); tx_result.unwrap().assert_success(); } @@ -294,8 +348,18 @@ fn non_refundable_transfer_create_named_account() { #[test] fn non_refundable_transfer_create_near_implicit_account() { let new_account_id = near_implicit_test_account(); - let tx_result = - exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + let tx_result = exec_transfer( + &mut setup_env(), + signer(), + new_account_id, + 1, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: true, + deploy_contract: false, + }, + ); tx_result.unwrap().assert_success(); } @@ -303,16 +367,36 @@ fn non_refundable_transfer_create_near_implicit_account() { #[test] fn non_refundable_transfer_create_eth_implicit_account() { let new_account_id = eth_implicit_test_account(); - let tx_result = - exec_transfer(&mut setup_env(), signer(), new_account_id, 1, true, true, true, false); + let tx_result = exec_transfer( + &mut setup_env(), + signer(), + new_account_id, + 1, + TransferConfig { + nonrefundable: true, + account_creation: true, + implicit_account_creation: true, + deploy_contract: false, + }, + ); tx_result.unwrap().assert_success(); } /// Non-refundable transfer is rejected on existing account. #[test] fn reject_non_refundable_transfer_existing_account() { - let tx_result = - exec_transfer(&mut setup_env(), signer(), receiver(), 1, true, false, false, false); + let tx_result = exec_transfer( + &mut setup_env(), + signer(), + receiver(), + 1, + TransferConfig { + nonrefundable: true, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, + ); let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; assert!(matches!( status, @@ -333,7 +417,18 @@ fn reject_non_refundable_transfer_in_older_versions() { let mut env = setup_env_with_protocol_version(Some( ProtocolFeature::NonRefundableBalance.protocol_version() - 1, )); - let tx_result = exec_transfer(&mut env, signer(), receiver(), 1, true, false, false, false); + let tx_result = exec_transfer( + &mut env, + signer(), + receiver(), + 1, + TransferConfig { + nonrefundable: true, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, + ); assert_eq!( tx_result, Err(InvalidTxError::ActionsValidation( diff --git a/runtime/near-wallet-contract/res/wallet_contract.wasm b/runtime/near-wallet-contract/res/wallet_contract.wasm index a46d674bf9901add72983dc3e5f21f48e350b693..8ff0d36de140c0024c2003facf13f92deae42b08 100755 GIT binary patch delta 18692 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 delta 18306 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 Result<(), StorageError> { + account.set_amount(account.amount().checked_add(deposit).ok_or_else(|| { + StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) + })?); + Ok(()) +} + +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +pub(crate) fn action_nonrefundable_storage_transfer( account: &mut Account, deposit: Balance, - nonrefundable: bool, ) -> Result<(), StorageError> { - if nonrefundable { - assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else( - || { - StorageError::StorageInconsistentState( - "Non-refundable account balance integer overflow".to_string(), - ) - }, - )?); - } else { - account.set_amount(account.amount().checked_add(deposit).ok_or_else(|| { - StorageError::StorageInconsistentState("Account balance integer overflow".to_string()) - })?); - } + account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else(|| { + StorageError::StorageInconsistentState( + "Non-refundable account balance integer overflow".to_string(), + ) + })?); Ok(()) } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 2c388a739dd..3ce0f17605b 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -1549,7 +1549,13 @@ fn action_transfer_or_implicit_account_creation( actor_id: &mut AccountId, ) -> Result<(), RuntimeError> { Ok(if let Some(account) = account.as_mut() { - action_transfer(account, deposit, nonrefundable)?; + if nonrefundable { + assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + action_nonrefundable_storage_transfer(account, deposit)?; + } else { + action_transfer(account, deposit)?; + } // Check if this is a gas refund, then try to refund the access key allowance. if is_refund && action_receipt.signer_id == receipt.receiver_id { try_refund_allowance( From 329dcd818cbde514387e2ce9156595dc7812c8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Mon, 22 Jan 2024 11:53:43 +0100 Subject: [PATCH 27/36] Both refundable and nonrefundable transfers --- core/primitives/src/action/mod.rs | 4 + .../client/features/nonrefundable_transfer.rs | 320 ++++++++++-------- runtime/runtime/src/actions.rs | 17 +- runtime/runtime/src/lib.rs | 6 +- 4 files changed, 201 insertions(+), 146 deletions(-) diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index cb3cba94686..ca279173ca0 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -194,6 +194,10 @@ pub enum Action { DeleteAccount(DeleteAccountAction), Delegate(Box), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + /// Makes a non-refundable transfer for storage allowance. + /// Only possible during new account creation. + /// For implicit account creation, it has to be the first action + /// in the receipt. Following regular transfers are allowed in the same receipt. NonrefundableStorageTransfer(NonrefundableStorageTransferAction), } // Note: If this number ever goes down, please adjust the equality accordingly. Otherwise, diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 3f6df8af353..1a977ba64e2 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -13,12 +13,12 @@ use near_crypto::{InMemorySigner, KeyType, PublicKey}; use near_primitives::errors::{ ActionError, ActionErrorKind, ActionsValidationError, InvalidTxError, TxExecutionError, }; -use near_primitives::test_utils::{eth_implicit_test_account, near_implicit_test_account}; use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, NonrefundableStorageTransferAction, SignedTransaction, TransferAction, }; use near_primitives::types::{AccountId, Balance}; +use near_primitives::utils::{derive_eth_implicit_account_id, derive_near_implicit_account_id}; use near_primitives::version::{ProtocolFeature, ProtocolVersion}; use near_primitives::views::{ ExecutionStatusView, FinalExecutionOutcomeView, QueryRequest, QueryResponseKind, @@ -31,6 +31,26 @@ use testlib::fees_utils::FeeHelper; use crate::node::RuntimeNode; +#[derive(Clone, Debug)] +struct Transfers { + regular_amount: Balance, + nonrefundable_amount: Balance, + nonrefundable_transfer_first: bool, +} + +const TEST_CASES: [Transfers; 3] = [ + Transfers { regular_amount: 0, nonrefundable_amount: 1, nonrefundable_transfer_first: true }, + Transfers { regular_amount: 1, nonrefundable_amount: 1, nonrefundable_transfer_first: true }, + Transfers { regular_amount: 1, nonrefundable_amount: 1, nonrefundable_transfer_first: false }, +]; + +struct TransferConfig { + transfers: Transfers, + account_creation: bool, + implicit_account_creation: bool, + deploy_contract: bool, +} + /// Default sender to use in tests of this module. fn sender() -> AccountId { "test0".parse().unwrap() @@ -108,22 +128,14 @@ fn execute_transaction_from_actions( tx_result } -struct TransferConfig { - nonrefundable: bool, - account_creation: bool, - implicit_account_creation: bool, - deploy_contract: bool, -} - -/// Submits a transfer (either regular or non-refundable). +/// Submits a transfer (regular, non-refundable, or both). /// /// This methods checks that the balance is subtracted from the sender and added /// to the receiver, if the status was ok. No checks are done on an error. -fn exec_transfer( +fn exec_transfers( env: &mut TestEnv, signer: InMemorySigner, receiver: AccountId, - deposit: Balance, config: TransferConfig, ) -> Result { let sender_pre_balance = env.query_balance(sender()); @@ -144,12 +156,18 @@ fn exec_transfer( }))); } - if config.nonrefundable { + if config.transfers.nonrefundable_transfer_first && config.transfers.nonrefundable_amount > 0 { actions.push(Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { - deposit, + deposit: config.transfers.nonrefundable_amount, + })); + } + if config.transfers.regular_amount > 0 { + actions.push(Action::Transfer(TransferAction { deposit: config.transfers.regular_amount })); + } + if !config.transfers.nonrefundable_transfer_first && config.transfers.nonrefundable_amount > 0 { + actions.push(Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { + deposit: config.transfers.nonrefundable_amount, })); - } else { - actions.push(Action::Transfer(TransferAction { deposit })); } if config.deploy_contract { @@ -171,16 +189,18 @@ fn exec_transfer( } let gas_cost = outcome.gas_cost(); - assert_eq!(sender_pre_balance - deposit - gas_cost, env.query_balance(sender())); + assert_eq!( + sender_pre_balance + - config.transfers.regular_amount + - config.transfers.nonrefundable_amount + - gas_cost, + env.query_balance(sender()) + ); + let receiver_expected_amount_after = receiver_before_amount + config.transfers.regular_amount; + let receiver_expected_non_refundable_after = + receiver_before_nonrefundable + config.transfers.nonrefundable_amount; let receiver_after = env.query_account(receiver); - let (receiver_expected_amount_after, receiver_expected_non_refundable_after) = - if config.nonrefundable { - (receiver_before_amount, receiver_before_nonrefundable + deposit) - } else { - (receiver_before_amount + deposit, receiver_before_nonrefundable) - }; - assert_eq!(receiver_after.amount, receiver_expected_amount_after); assert_eq!(receiver_after.nonrefundable, receiver_expected_non_refundable_after); @@ -206,16 +226,21 @@ fn deleting_account_with_non_refundable_storage() { KeyType::ED25519, new_account_id.as_str(), ); - let nonrefundable_deposit = NEAR_BASE; + let regular_amount = 10u128.pow(20); + let nonrefundable_amount = NEAR_BASE; // Create account with non-refundable storage. + // Send some NEAR (refundable) so that the new account is able to pay the gas for its deletion in the next transaction. // Deploy a contract that does not fit within Zero-balance account limit. - let create_account_tx_result = exec_transfer( + let create_account_tx_result = exec_transfers( &mut env, signer(), new_account_id.clone(), - nonrefundable_deposit, TransferConfig { - nonrefundable: true, + transfers: Transfers { + regular_amount, + nonrefundable_amount, + nonrefundable_transfer_first: true, + }, account_creation: true, implicit_account_creation: false, deploy_contract: true, @@ -223,22 +248,6 @@ fn deleting_account_with_non_refundable_storage() { ); create_account_tx_result.unwrap().assert_success(); - // Send some NEAR (refundable) so that the new account is able to pay the gas for its deletion in the next transaction. - let deposit = 10u128.pow(20); - let send_money_tx_result = exec_transfer( - &mut env, - signer(), - new_account_id.clone(), - deposit, - TransferConfig { - nonrefundable: false, - account_creation: false, - implicit_account_creation: false, - deploy_contract: false, - }, - ); - send_money_tx_result.unwrap().assert_success(); - // Delete the new account (that has 1 NEAR of non-refundable balance). let beneficiary_id = receiver(); let beneficiary_before = env.query_account(beneficiary_id.clone()); @@ -251,7 +260,7 @@ fn deleting_account_with_non_refundable_storage() { let beneficiary_after = env.query_account(beneficiary_id); assert_eq!( beneficiary_after.amount, - beneficiary_before.amount + deposit - fee_helper().prepaid_delete_account_cost() + beneficiary_before.amount + regular_amount - fee_helper().prepaid_delete_account_cost() ); assert_eq!(beneficiary_after.nonrefundable, beneficiary_before.nonrefundable); } @@ -267,13 +276,16 @@ fn non_refundable_balance_cannot_be_transferred() { new_account_id.as_str(), ); // The `new_account` is created with 1 NEAR non-refundable balance. - let create_account_tx_result = exec_transfer( + let create_account_tx_result = exec_transfers( &mut env, signer(), new_account_id.clone(), - NEAR_BASE, TransferConfig { - nonrefundable: true, + transfers: Transfers { + regular_amount: 0, + nonrefundable_amount: NEAR_BASE, + nonrefundable_transfer_first: true, + }, account_creation: true, implicit_account_creation: false, deploy_contract: false, @@ -283,13 +295,16 @@ fn non_refundable_balance_cannot_be_transferred() { // Although `new_account` has 1 NEAR non-refundable balance, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. for nonrefundable in [false, true] { - let transfer_tx_result = exec_transfer( + let transfer_tx_result = exec_transfers( &mut env, new_account.clone(), receiver(), - 1, TransferConfig { - nonrefundable, + transfers: Transfers { + regular_amount: if nonrefundable { 0 } else { 1 }, + nonrefundable_amount: if nonrefundable { 1 } else { 0 }, + nonrefundable_transfer_first: true, + }, account_creation: false, implicit_account_creation: false, deploy_contract: false, @@ -310,13 +325,16 @@ fn non_refundable_balance_cannot_be_transferred() { fn non_refundable_balance_allows_1kb_state_with_zero_balance() { let mut env = setup_env(); let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); - let tx_result = exec_transfer( + let tx_result = exec_transfers( &mut env, signer(), new_account_id, - NEAR_BASE / 5, TransferConfig { - nonrefundable: true, + transfers: Transfers { + regular_amount: 0, + nonrefundable_amount: NEAR_BASE / 5, + nonrefundable_transfer_first: true, + }, account_creation: true, implicit_account_creation: false, deploy_contract: true, @@ -328,82 +346,113 @@ fn non_refundable_balance_allows_1kb_state_with_zero_balance() { /// Non-refundable transfer successfully adds non-refundable balance when creating named account. #[test] fn non_refundable_transfer_create_named_account() { - let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); - let tx_result = exec_transfer( - &mut setup_env(), - signer(), - new_account_id, - 1, - TransferConfig { - nonrefundable: true, - account_creation: true, - implicit_account_creation: false, - deploy_contract: false, - }, - ); - tx_result.unwrap().assert_success(); + for (index, transfers) in TEST_CASES.iter().enumerate() { + let account_name = format!("subaccount{}.test0", index).to_string(); + let new_account_id: AccountId = account_name.parse().unwrap(); + let tx_result = exec_transfers( + &mut setup_env(), + signer(), + new_account_id, + TransferConfig { + transfers: transfers.clone(), + account_creation: true, + implicit_account_creation: false, + deploy_contract: false, + }, + ); + tx_result.unwrap().assert_success(); + } } /// Non-refundable transfer successfully adds non-refundable balance when creating NEAR-implicit account. #[test] fn non_refundable_transfer_create_near_implicit_account() { - let new_account_id = near_implicit_test_account(); - let tx_result = exec_transfer( - &mut setup_env(), - signer(), - new_account_id, - 1, - TransferConfig { - nonrefundable: true, - account_creation: true, - implicit_account_creation: true, - deploy_contract: false, - }, - ); - tx_result.unwrap().assert_success(); + for (index, transfers) in TEST_CASES.iter().enumerate() { + let public_key = + PublicKey::from_seed(KeyType::ED25519, &format!("near{}", index).to_string()); + let new_account_id = derive_near_implicit_account_id(public_key.unwrap_as_ed25519()); + let tx_result = exec_transfers( + &mut setup_env(), + signer(), + new_account_id.clone(), + TransferConfig { + transfers: transfers.clone(), + account_creation: true, + implicit_account_creation: true, + deploy_contract: false, + }, + ); + if transfers.nonrefundable_transfer_first { + tx_result.unwrap().assert_success(); + } else { + // Non-refundable transfer must be the first action if it appears in implicit account creation transaction. + let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; + assert!(matches!( + status, + ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + )) if *account_id == new_account_id, + )); + } + } } /// Non-refundable transfer successfully adds non-refundable balance when creating ETH-implicit account. #[test] fn non_refundable_transfer_create_eth_implicit_account() { - let new_account_id = eth_implicit_test_account(); - let tx_result = exec_transfer( - &mut setup_env(), - signer(), - new_account_id, - 1, - TransferConfig { - nonrefundable: true, - account_creation: true, - implicit_account_creation: true, - deploy_contract: false, - }, - ); - tx_result.unwrap().assert_success(); + for (index, transfers) in TEST_CASES.iter().enumerate() { + let public_key = + PublicKey::from_seed(KeyType::SECP256K1, &format!("eth{}", index).to_string()); + let new_account_id = derive_eth_implicit_account_id(public_key.unwrap_as_secp256k1()); + let tx_result = exec_transfers( + &mut setup_env(), + signer(), + new_account_id.clone(), + TransferConfig { + transfers: transfers.clone(), + account_creation: true, + implicit_account_creation: true, + deploy_contract: false, + }, + ); + if transfers.nonrefundable_transfer_first { + tx_result.unwrap().assert_success(); + } else { + // Non-refundable transfer must be the first action if it appears in implicit account creation transaction. + let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; + assert!(matches!( + status, + ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + )) if *account_id == new_account_id, + )); + } + } } /// Non-refundable transfer is rejected on existing account. #[test] fn reject_non_refundable_transfer_existing_account() { - let tx_result = exec_transfer( - &mut setup_env(), - signer(), - receiver(), - 1, - TransferConfig { - nonrefundable: true, - account_creation: false, - implicit_account_creation: false, - deploy_contract: false, - }, - ); - let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; - assert!(matches!( - status, - ExecutionStatusView::Failure(TxExecutionError::ActionError( - ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } - )) if *account_id == receiver(), - )); + for transfers in TEST_CASES { + let tx_result = exec_transfers( + &mut setup_env(), + signer(), + receiver(), + TransferConfig { + transfers, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, + ); + let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; + assert!(matches!( + status, + ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + )) if *account_id == receiver(), + )); + } } /// During the protocol upgrade phase, before the voting completes, we must not @@ -417,25 +466,26 @@ fn reject_non_refundable_transfer_in_older_versions() { let mut env = setup_env_with_protocol_version(Some( ProtocolFeature::NonRefundableBalance.protocol_version() - 1, )); - let tx_result = exec_transfer( - &mut env, - signer(), - receiver(), - 1, - TransferConfig { - nonrefundable: true, - account_creation: false, - implicit_account_creation: false, - deploy_contract: false, - }, - ); - assert_eq!( - tx_result, - Err(InvalidTxError::ActionsValidation( - ActionsValidationError::UnsupportedProtocolFeature { - protocol_feature: "NonRefundableBalance".to_string(), - version: ProtocolFeature::NonRefundableBalance.protocol_version() - } - )) - ); + for transfers in TEST_CASES { + let tx_result = exec_transfers( + &mut env, + signer(), + receiver(), + TransferConfig { + transfers, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, + ); + assert_eq!( + tx_result, + Err(InvalidTxError::ActionsValidation( + ActionsValidationError::UnsupportedProtocolFeature { + protocol_feature: "NonRefundableBalance".to_string(), + version: ProtocolFeature::NonRefundableBalance.protocol_version() + } + )) + ); + } } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 25736c6f070..824e3674c04 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -962,7 +962,7 @@ pub(crate) fn check_account_existence( account: &Option, account_id: &AccountId, config: &RuntimeConfig, - is_the_only_action: bool, + only_transfers: bool, is_refund: bool, #[cfg_attr( not(feature = "protocol_feature_nonrefundable_transfer_nep491"), @@ -1002,7 +1002,7 @@ pub(crate) fn check_account_existence( if account.is_none() { return check_transfer_to_nonexisting_account( config, - is_the_only_action, + only_transfers, account_id, is_refund, ); @@ -1013,7 +1013,7 @@ pub(crate) fn check_account_existence( if account.is_none() { return check_transfer_to_nonexisting_account( config, - is_the_only_action, + only_transfers, account_id, is_refund, ); @@ -1024,9 +1024,8 @@ pub(crate) fn check_account_existence( // receipt which is allowed. Checking for the first action of // the receipt being a `CreateAccount` action serves this // purpose. - // For implicit accounts, it is impossible that the account was - // created by a prior action in the receipt because they must be - // created with a singleton receipt. + // For implicit accounts creation with non-refundable storage + // we require that this is the first action in the receipt. return Err(ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id: account_id.clone(), } @@ -1060,18 +1059,18 @@ pub(crate) fn check_account_existence( fn check_transfer_to_nonexisting_account( config: &RuntimeConfig, - is_the_only_action: bool, + only_transfers: bool, account_id: &AccountId, is_refund: bool, ) -> Result<(), ActionError> { if config.wasm_config.implicit_account_creation - && is_the_only_action + && only_transfers && account_is_implicit(account_id, config.wasm_config.eth_implicit_accounts) && !is_refund { // OK. It's implicit account creation. // Notes: - // - The transfer action has to be the only action in the transaction to avoid + // - Transfer actions have to be the only actions in the transaction to avoid // abuse by hijacking this account with other public keys or contracts. // - Refunds don't automatically create accounts, because refunds are free and // we don't want some type of abuse. diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 3ce0f17605b..88f0588cfc3 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -307,7 +307,9 @@ impl Runtime { // TODO(#8806): Support compute costs for actions. For now they match burnt gas. result.compute_usage = exec_fees; let account_id = &receipt.receiver_id; - let is_the_only_action = actions.len() == 1; + let only_transfers = actions.iter().all(|action| { + matches!(action, Action::Transfer(_) | Action::NonrefundableStorageTransfer(_)) + }); let is_refund = receipt.predecessor_id.is_system(); let receipt_starts_with_create_account = @@ -318,7 +320,7 @@ impl Runtime { account, account_id, &apply_state.config, - is_the_only_action, + only_transfers, is_refund, receipt_starts_with_create_account, ) { From ffa1dc4209ec57a133acf2d6daafe2fee19c06f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Fri, 26 Jan 2024 15:23:34 +0100 Subject: [PATCH 28/36] implicit_account_creation_eligible --- runtime/runtime/src/actions.rs | 16 +++++----------- runtime/runtime/src/lib.rs | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 7bc6dd7b924..724cd9c1be0 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -963,8 +963,7 @@ pub(crate) fn check_account_existence( account: &Option, account_id: &AccountId, config: &RuntimeConfig, - only_transfers: bool, - is_refund: bool, + implicit_account_creation_eligible: bool, #[cfg_attr( not(feature = "protocol_feature_nonrefundable_transfer_nep491"), allow(unused_variables) @@ -1003,9 +1002,8 @@ pub(crate) fn check_account_existence( if account.is_none() { return check_transfer_to_nonexisting_account( config, - only_transfers, account_id, - is_refund, + implicit_account_creation_eligible, ); } } @@ -1014,9 +1012,8 @@ pub(crate) fn check_account_existence( if account.is_none() { return check_transfer_to_nonexisting_account( config, - only_transfers, account_id, - is_refund, + implicit_account_creation_eligible, ); } else if !receipt_starts_with_create_account { // If the account already existed before the current receipt, @@ -1053,14 +1050,12 @@ pub(crate) fn check_account_existence( fn check_transfer_to_nonexisting_account( config: &RuntimeConfig, - only_transfers: bool, account_id: &AccountId, - is_refund: bool, + implicit_account_creation_eligible: bool, ) -> Result<(), ActionError> { if config.wasm_config.implicit_account_creation - && only_transfers + && implicit_account_creation_eligible && account_is_implicit(account_id, config.wasm_config.eth_implicit_accounts) - && !is_refund { // OK. It's implicit account creation. // Notes: @@ -1488,7 +1483,6 @@ mod tests { &RuntimeConfig::test(), false, false, - false, ), Err(ActionErrorKind::AccountDoesNotExist { account_id: sender_id.clone() }.into()) ); diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 15b866ff342..b229f795d40 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -344,10 +344,21 @@ impl Runtime { // TODO(#8806): Support compute costs for actions. For now they match burnt gas. result.compute_usage = exec_fees; let account_id = &receipt.receiver_id; + let is_refund = receipt.predecessor_id.is_system(); + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let is_the_only_action = actions.len() == 1; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] let only_transfers = actions.iter().all(|action| { matches!(action, Action::Transfer(_) | Action::NonrefundableStorageTransfer(_)) }); - let is_refund = receipt.predecessor_id.is_system(); + + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let implicit_account_creation_eligible = is_the_only_action && !is_refund; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let implicit_account_creation_eligible = only_transfers && !is_refund; let receipt_starts_with_create_account = matches!(actions.get(0), Some(Action::CreateAccount(_))); @@ -357,8 +368,7 @@ impl Runtime { account, account_id, &apply_state.config, - only_transfers, - is_refund, + implicit_account_creation_eligible, receipt_starts_with_create_account, ) { result.result = Err(e); From 5cbf7dcd34944af56daf0c8ddba02c829b99b7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Fri, 26 Jan 2024 16:50:23 +0100 Subject: [PATCH 29/36] Add comments --- chain/rosetta-rpc/src/adapters/mod.rs | 6 +++--- core/primitives/src/views.rs | 1 - .../client/features/nonrefundable_transfer.rs | 9 +++++++++ .../res/wallet_contract.wasm | Bin 94343 -> 93368 bytes 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 109b03cd389..cddd2038420 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -2,7 +2,6 @@ use actix::Addr; use near_chain_configs::Genesis; use near_client::ViewClientActor; use near_o11y::WithSpanContextExt; -use near_primitives::transaction::TransferAction; use validated_operations::ValidatedOperation; pub(crate) mod nep141; @@ -326,8 +325,8 @@ impl From for Vec { ); } - near_primitives::transaction::Action::Transfer(TransferAction { deposit }) => { - let transfer_amount = crate::models::Amount::from_yoctonear(deposit); + near_primitives::transaction::Action::Transfer(action) => { + let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); let sender_transfer_operation_id = crate::models::OperationIdentifier::new(&operations); @@ -355,6 +354,7 @@ impl From for Vec { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] // Note(jakmeier): Both refundable and non-refundable transfers are considered as available balance. + // TODO(nonrefundable) Merge with the arm above on stabilization. near_primitives::transaction::Action::NonrefundableStorageTransfer(action) => { let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index e99b8061481..cd161898798 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -31,7 +31,6 @@ use crate::transaction::{ ExecutionStatus, FunctionCallAction, PartialExecutionOutcome, PartialExecutionStatus, SignedTransaction, StakeAction, TransferAction, }; - use crate::types::{ AccountId, AccountWithPublicKey, Balance, BlockHeight, EpochHeight, EpochId, FunctionArgs, Gas, Nonce, NumBlocks, ShardId, StateChangeCause, StateChangeKind, StateChangeValue, diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 1a977ba64e2..197cda0e01a 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -33,11 +33,15 @@ use crate::node::RuntimeNode; #[derive(Clone, Debug)] struct Transfers { + /// Regular transfer amount (if any). regular_amount: Balance, + /// Non-refundable transfer amount (if any). nonrefundable_amount: Balance, + /// Whether non-refundable transfer action should be first in the receipt. nonrefundable_transfer_first: bool, } +/// Different `Transfers` configurations, where we only test cases where non-refundable transfer happens. const TEST_CASES: [Transfers; 3] = [ Transfers { regular_amount: 0, nonrefundable_amount: 1, nonrefundable_transfer_first: true }, Transfers { regular_amount: 1, nonrefundable_amount: 1, nonrefundable_transfer_first: true }, @@ -45,9 +49,13 @@ const TEST_CASES: [Transfers; 3] = [ ]; struct TransferConfig { + /// Describes transfers configuration we are interested in. transfers: Transfers, + /// True if the receipt should create account. account_creation: bool, + /// Differentaties between named and implicit account creation, if `account_creation` is true. implicit_account_creation: bool, + /// Whether the last action in the receipt should deploy a contract. deploy_contract: bool, } @@ -129,6 +137,7 @@ fn execute_transaction_from_actions( } /// Submits a transfer (regular, non-refundable, or both). +/// Can possibly create an account or deploy a contract, depending on the `config`. /// /// This methods checks that the balance is subtracted from the sender and added /// to the receiver, if the status was ok. No checks are done on an error. diff --git a/runtime/near-wallet-contract/res/wallet_contract.wasm b/runtime/near-wallet-contract/res/wallet_contract.wasm index 8ff0d36de140c0024c2003facf13f92deae42b08..a46d674bf9901add72983dc3e5f21f48e350b693 100755 GIT binary patch delta 18306 zcmc(G34ByV^8c$hlT4DCBrg}a$(tcSLI_EK9E2+m1VlL%k6kaWpack4P}eH~!X>O` z>A=GU6c7>FDCn?)ifd356jXFs6^()z1(j8BQPD;Cf2-fTNdmh6{eAww&*u;H^sDOX z>gww1>gt066Q#vEV&fc& zszl*Kjf#qoazqjSL@99z3970(REGogq8!L5R{kS=E3uAti4GON4peePIaG2asqH|M z5S_r>$U)Ijq(&vDq(()7V>PKn3Z=yp09D0jD%eHErN^N$6-cFh2EO7|HA;n;(NWP- ziukB#M|6|}4MTJ^92FO@vV>?c8VvbQJ5`N`kdTspVxwYJ1^?t9X9&%pE3OD}kdhP= zn(U}1qo$@&aaSwG+{L?PUME&N(`meT&Y4y{AFUYV9(whd-#tJN(nflc{z`uO3+<-s zZdgEdw3_}*PtX?HPH)pY)J&h#7j&FX(4f9EMhyEqJxoJBqnD}hE$Y{&|KL4z?|n5i zkB-rI^e0+QkI*{WK)dKgy5r7&&>_10JvvD5(rG$H-_yV747vYF-(Zj;zIo00t++L0 zB-Q$Ngg^~7VsB_R{X?7%Euf8}gDW53V_e;+R?K%5QGlO>LEH#XOJlbkmQ7j270l$~SGO1Y{59{LE z_%$kMuZg7aQNhaF#msPD5ZWWwgm)?`!pIu&+M9FHqR*kLMXIJdbShFky2A*o&=ve- zsY;Qm0Ig)JN-=*4N5n5QP>gfOoBiU*CEB}Pzao@>w7ahw-Zpu*+)av9u3yR}{BlV-NXXh;phCHh5;9cwN&Y)-kQ&H30rg>k?#N@9z}Tj?@p{ z_V``OhWbD=lP6X(XT`m}er zd!7Uanv?((&rn_fkaS@IAeKu!0U(Bfhyb855E%fX83+#mQEmv(l?C_&O^Z<(z35|c ze?}%96&dtwWm%}YR#6`%}iu3WGy-@+@0lPIC_Ho@X(`Zj|VtUsw*RuVLP+Jfi=QARr1+nOUMtCfQziSR{ z6Gytvj;}eu=wc|ae}%D6R;u?;%6%hLJ-A+U?^z6AQ_-_K%@b>Tj?;dGd#{FdDT82I zr2QzO3RBaYS#Ylk_#CSsMOO^=C2AS}6=j7TVt--0PPqysoEb?ThgOIgpx2WaRl1ItniZC0OKOHd676-I6NjwK|RvmWMvtO zyHhl=})*3&zpYjJmaTijBdlKpmoBp<$!rO=KuLRTOI|AhG*m|{TB7aNKz z=v@(6GAL%B>_&@QeqBr`d05@IPdIuN(h@Fh=eU>dmqg~}Q{q;f0HX??Ul(Ym&#AkN6=K8XYkn0!dg$6%Nw0ehtDYXJ z&0BuOwHHx`>fzur`HE+M72o~Jm%6QztrjTC!U4QGnr*?AZNU{HV_0WeA%+agtKR+u z#MJ_81%wSZ+G0Y1sh2cMdCI}5dcc39#Mo{&3^W$Trr}z9+HejB=1?1qq={>TvWWa) z!*dDZ6^$4|_2Rw}!(Ds6WFg-d?~W*N9SVF$jLgogeVg$zOfVkiJ{nBrC z_b6`=+Aba&<*PpPCUdv-2`e;e-)fBs2WDeyjA6%`+ogs_bg@}T(cyXv~2A%W|GQK5D*TR7>!3}>uaAh!B zYZCRBb@bUGW77hb+ipV*2FmURaeQ>i@Uy(s7#HxzH4{sa_LPfi{k)asX zMHZk_q?1cpSBqo(iMAhuHx@HsxS_5nHE@it+u?u+7Lvi zL4l@QHxQI75=)RlrOGLm3lj3GC}kXQuLik!wJ}1_j`q1|OimN})$PTAtLw-s{(f~% zbi+b!`za>#hIl%N$T4M7G08ofECyMS+!q?D@|smophU2V&)e`7hbFzD z2COc?Bu;!YW+cUme%JKG@9b+jMQFN`tNaPQh!&4tlb3;TRCXM{r<4lq(^gG{4T{vn z>1z_)E&MeRbL{}i^pClACaK>o5qq!8qbBj=b<@O}etEAhSkhU;jZ@Z7kXz7U&l+#3Gf5_I1P)eywrlsNdT zgrc?g0zE-M>_HC^B!>?2MjAG;T^!+7%k&70gFz^@D}N~V-_!+>>kl^-!_SVsIX=&z zpl+hvhHZk&q~>iCgfjbJ`Oa`_C&bK~^W1tQ#vILJ;6=mD$Af0P#=qtkLq#7x8J89J zxurSQqNP#oOOZUj6MZGh#y4=Z`_1@~9PRd>kV0RJ-%lt*w7YsjUwj{)(3`&YCrq3} zScaCCUr)zHbNP_Y2Ofqx*uP_mA;WpQ(SqPmNj8MAM_e^&1X!(~ltcd%Z%!)dVhaL? zBS%*IG9Veuj_stx2V2il%d~Gq-mQa!omnRCyEWr?kMUeII$1T159Tv;K+eyXnTuu8 z#+rS=+rGzWUd&Y8v~Mj6tFCs6A;T}r3bb#<54UFY-o^1GVWrW7Ld_{Nu8yhTO>M9 z>7H`*5tQ)WI=IYx@ad`07K+(ZhGRT;PpM0@_E?HXWgA6ZEdM;NK8Jyw>a?jP*eY$B z+Lb;Mf1f%oupv_rFY~Bl#I#SukZE0Fx67Ssz~3L^m|V;ZiR-}poY>$a3-dXr=rl4? zK`Sts3XHRJC}MWuoPQ%WRZDO0<^JAMD@4@Ao6|a{FXxW&{uZ07iUGuD0GL2UYCN)i zBBruy2dnGNR=px-jRj1>G^^i@s>U7#N*r71t$u^ZS=|9-Kx$Kblqo-DbY{f1j?S=W zkfFArWGjnn!J}B=9jjJhD0#{%M)a15Fd(v(x&Q@s%+^tc(}-nxvECJ_PGk;j6$~Z! zO2x7XwajpW_B6}kb3`f}M>vdxQiZqhyR3i`m2@ZXFwibDQ4D4RQMC#wwYX)vkG>J} zruUcI(oW*UbT8d5+RsSusjJe%Al9>50En{{-i~N<=b_0IM5K@=AdEpw$osmefZ`#} z1_#bn=1z+@zf6KVd1=P!mQ7$NhhH{^MMT;>Ave4Yiq9A{_gu7&)B-PZ;OFnSj-UZ7Y!4S+Q|W zUYx{w9q52Ofsz}nFXn8tOa}qztGADGHNL{lJ}1)dxQ_niuew8bxK0I7tMBTPE$5#X z8*;RhRG~?**pUS|q}ZWL6&o5{@JaE_T|HfmFEg`eMfTmxa}P=`YgsTjnWL>CJxpr| z;A{49jkW$0cVAAlLloS5U++3rhh@TnKpQI@_5{W%F0fbfc0Af_cyhp{4Hv0qZY(AG zzq%J^Wsmv2)n_Sv%}%ak2UVz{yYvX-%tpp;5=C?GcQpsT_s%VD88{UK7cs9xbZ|Ql z1T4^;?xrNsWsY0iGH(Q(7EjGfikmOZgoV-ev5{g!D;nlkxiDO?!zz(gTOQ9_8VMs) zaH?}>g)daBt?f;r;&5$G{6;M(h7k>3kb&Px3%n^V-bzPUM|>_m;$zQ&Y3g-}^$YkM z@2?BIZkO92FCPNwF7fGt%ry9o7^oHc=1_v0RP->krgLkV3$Hb|wxJ`5UiNcRq%rm<%yR zB&f|b;>sn{)W2;I&oA+E_VJQiIO}(RHfNLX&uV9}V1E`~p*t&#&bo7ownRLB|6#gc z%w1~8-*jL`eM*_3Sz^qQ1=4EFDC%SW$m3j-rRxcN8TnI=KT_9v?BYBvsU| z=-kEHRH};mD_km6(|xM#$7^VcPYpP7UbejAmkoM#wgXE9#&I#vNiVNzI6rnEN--`d zUWWrp*Bw5k!WV+=!~2TveB6kh>2nwnLsNA}MXAE0S6zWkx^sXw#v6*vxfl@+qASb= z#i*exVU9TYF?MZ0&kdZb8FL8*Q~^z*$!czshwr!AEdF0_5-qZk)({)K`OkC;%V0r` z29{7;Wcem3yNwdz<`7&Dj5hbX4oH`>RCHK1OFgjDZ?1Au5Vzr>JF3_GQFc?SXErHr z7%dq(Z*;(Lqbzf_DQi>$h1+GJt4(1I1qu(!!l<@|YM^lO1Keb)EQ~^%x)SX+vher7 zPO<6FU0rf1^E$=He_jcBFrs8+jf^C5&+6;od=9P7N+b9YAW?xeVEfAuBYR||Lxirm z%c=&ZTy4pkn)Y0Q+h|(>)?uvUbCZO%-jaZ#0@`$3LL3sr*qeWgMl4^ui{^@v56>FW zmb)`p!4}YUF~<0TJ7A;-e3_Y-+n|@$5x9>I9tYw~o_KgOHfIYZY zG;J6X(K6G-(~ILo*`vMV+e{9JH95qCk6!z$1?I=;l%CSoS{ocbVwU?@H@K}|Hs+}fkBC1$mU}UecYkCaA+m~p{o|)dJ-c3r zCm!Ie!&3)PeW3C6n|i26*NgWyjcfM^+Xgfnf`!C5 zAgNXD5i$Jn_VD>rA1{oOOT$qvfPXqCCT#Ri0WaW2h_;)3 zAU2g`e7w=0@|Ss}ZhKBV+;AIbVNYhpHthgi(6pV#_UA;uCp&ZLtxsO5Zhu~EfAW(d z!7W*-f_pXa^4tz@*RoS9h2vnfb}>3aqT>T-F?w5Omz8XaGWJCUZ1>voiNwmRA|%dL zR>2~-wNno~Ec$QlJ78x6cc-gS_ax_g$T;*Af4uC_9+%qUgLm*-`3z&Tj4x#Vh-xhO zDUDS7Bk6ijz-|4=&cL z8pQR_Jm0?g2@rc-yci++F6Y82fj8hwH9 zHRApL=Vp`Iv`J*W&^_hg%Pdzw(irdvF3tX`7oH_t_T;>H1um;^f3Y{kZew%60ScDR zP{^uk+x)NmEuLth_~4~}_)Tp5*tJgvw@byp8;`3?i~Y^Jb1CyP>5H2AR}UGWTqcTKY#L`5Kb^R$>eW2;jXLrAt6TMxvayavJ?qZaukq-EfN=YFYPDGP+A|UBq|Zz?Hm?z5 z_L{U(oZQ2@vx*`w02%18brN#-M@rJPYu!pIs{R8uZ`d%R1Evzcf$L2Nwp4r*RmsMj<`hM z78YAeIjr9H_81kZo7n&tW9xCz2w7M|4b;eJsqq_Q>P|rw9zch-!vFQ#DJsqNhrauA zNcFlpC?dF2BwpMVJ?#=q7>snAtYwd1%q29J*1CkVr6e)rgIU#WL!TDPKHBfrXuz0wc?4U{Nhj9H594acr-BxUYuySv0sff>RRj3Al_2&5tj5jsH=V4Cuytn zeOz!bsv;Afy$18m{qq3-#E-{NYHJVYWDn;KdN}{zKe=AIs@$X1?5dtRS_N0-&-^rn z*x!{M(-C=$I~K3P!99E|Nj!KglI4B)SZXKRY1Oc|3c9j-o}q|04{;8xpwDz#w)`_M zE%W<6>!pU<&aF;N{Cu;zYqf~_q6u33`io5bYR5C%1>Bj#Sd9H8{slbR700hGv^*Mi zf{i%o4cK)Gz-F}tWH)5hRdkQ^ckdqWaEs;Gf;EGnRkOt#%9;q|+jv8Hi@%F}U5eCQ zh--{B*!wzgo+v|m=?ul`S5o{lPCTk!WCH3XM&Gpss-jPog&>!s)WPqZc~ z4iJpS2z#Qc*qr`vdN1+u`QF{1W*u_$j{#M+XBcw(WE7_OlqUz^tKnW92NhTNlvJH} z6N|<03mvf0nR!8vwmV1i)z3p>-G$V=Jo%=?I`QNS2q#YP;GX%|1dBUqXV~d*gW67R z%v?&;(X)d;L5!o)9p)~gOjA=Si`JOERO*oQf-J?MB?3c!5%?{$P~1P6RVsB2Kgc%^ zIO^VIKB3Yeb>}1IDV6%>Eo%fCp=DMuw3HzQAAR~99A~IJGn-^(6_syvP`@0zQX_5v z&v7e=9@xEdgvxF63qqCyQkeT3RHz5*vxtZ^n5lfr!#_^2O?Og{oR&8YNARX$BTHt~ zzlq=AxivbEAyBoq&AXj+nBFz7388C}Yj-ovvzBtK=N0_a0#vVrP^EhEwH-Y|36FUG zW=`ZMe=l6fas5?xh|G6VDXyN%)C=oZ44_`LY{fm)6WhlX&(N@_SER7eK$eD z=+~^sTo6u$fhR}&5{k9y(Ox(Ahf^}mH$M-j$LI}nX#`y@-h3b}L>`WsO%XJL_M16w zbmxFM)=i572h&osr_Fz%K;1uAeB$d&hFPXkVir!vaqXe)XK7%h(m1T=UBQ!BJ`+;R z5gyzLo!DqFsWLBVTu-;0voDfRFRYhn_5u#7712 zv3#8^8(8!?3uNPCJn$3}ze3{ehBbuU7Jhvj`s!#;WT0Scn}R5hCxCxZ;^P7hM|u#_ z%LWcgysZh3CnA9VO5*zk=-r<10DkiqENFZHAK{V5_togxE?LksP!R6HorG-Ql*D%p z;KMwj0sNZd%qTN}cX>hr_(qAhH4)1DNXh3diMKlt!p9mC|0(e4*c}EKIW4~hfqsz{ zU=wCi4CeMhvnYn9P@}ma2B*tM%;p&ClG@^-U`)o&11wU&Ly5QVO^wiO2esE65lh!r zzsmNdOBW)4oYq5O4yxG6zr!}na<#h8kY`es$p&up++MI4@Zsw7=U-pF`Vm*L1J|zu z9`t|mDo^Ujh}9tXlXVtKxl&T@|y^ z?yLL%`fl8eZ$}MOYwm7GUW~wJ?WkLUEq-%r@%37%-V-859%(x>w5Un-s z{x7;ry~&o_{2)TK16>v3xr zAI%3$d|YEG*vapZK?y2erYs%fzDAB9>hdi!pE3k4%oHBf_gxgiH~7isw@J{(i)KtR z6;{6_k4gD4m$Sk!Sqg1E_tc9;(#R@R;&6EMN{}WFmz=F=&|b^UbI?iSr4J;%*h%vh zBj2wFoiyG!Wh*7$+df+zP!GFc5wi>LXkx&*2Dgq@H=)JPxr-cfNG;uOZcd@@ z+MycUTgyvKK0rM?*ZeAl(tXwuFdwZ$gj&fFN5EV^5)Q#>9Q)DCPQ|UQecp_CATQt6 zm~}18jazaDJERzYZo#5ATT>}jeg7x(Kq~dj{mt4R|I~;Mg+LbQQVzPd-poy-A@a~U z!Mq>WB!QD>vmuR!({}UcH2RIWVQMdmDY)IRwn>k+dGw!qiqusQ2p#AaDcC(SJx zYTCZ4TJ7?VBezy?JcQ7C3Kq<}WlnalGdFvwwb5VP`H^08558E5NEpx9mUq zz|V)?0~Oc$%a$dpfB50UrWCF;lGJK@KAWR6DUnW_lQSu;U9F`rmIc0BamPadM6;_i zY0QASTGnowac(|xdo3^;CDv;+aBgEgT+ONdz?yRZMdjTKX+O<13p&D3e=w^%B53@1 zuKCxF)P20ocKd=>42&9v46o4^`=4nK@wnL{1UWREX8A+s&1o z=qArkEUMQ9G0)94FLa{2SiZYE)2axY@aRG_J&XFQzsxmn&Vus+-Qp}7KFluPwy<^U zwxo^!1=i+paG~kVrre-t>vmk1O`ogXFGw|7{}Tb*nHoOza0kKjvefV7(Rm6)8%N4V!0ku_3wHOzK7hyw30I(hu8akKuAH*V_G=@as1 z&zg`oX<8MA>m*2%P(Gd&ZRQW91oNlv)GekUR8zdb4?_w^D%erdgKnVeDwl@$Zi=!y zfVbhF0rSAwFieJbRY74jiza8*(t>F0=(U${m&F@OU=*NzBg=B=JkBZc!;+_D9 z5F{Ip09A~6Pbth|l)0vqdLTPkO2vjv&;#9M)!Oh>z}!6>{tWPlR`9oguVC1`yf?Vr zY2MSDa>8%yoPT3yVDB^=dSf-Z(>&E1gVEpA`v96_PV7S++Sf&EN(yL7Z%~xl5)EI9 zvK=@`h}?Xv5BaFOxvviur=J2^2U^oKB^G%PBxfM?G}HQ0am-*8F^_AJCnBF_PV7q^ zhT7=5K=C@{+?{EGyi{*#`~Yz1jpS}mjzYjZO2y{EzLacU=uHVL`;jYNQKDirB^4!; zk+{M9J9i}ZqcAGQFhX1|Itk5ZAfGgD>g@8WS#!$qZI|Ui3huoPXPIOAQ}68BSgVIC zk^4|QYh2~6<$8JLM16AQ#PU1!eZX@!SDHKfQ%=MQfb9UDk2U|YfRS z-JYu9JOj3h#QFAA^VQ3#n|dzQ{2n=V-H|zz_B%pKP}a*V>qd#)@@CDMT{R)ExL0|v z;)0?Hy-M;6i}S}%npiNYsHmvC$M}MxaT5wldQ8eMC@SeyWcC_PKCLI57dPR6m*N>} z{(U%I=E=KcNqA~Fb<9<4SNAgPo~dp46~L_1U#%Bs{&56F3|7W^HRTo%-HpVP!iKBy z-M1B7i|_1K@Jf6K>CL7Q)Hy`+X-cx0pG!HHhq~x>~q~%907JpcEd?MPC05x6Jd8%^iJTX+IhB*E zbS$szIrar`BWF}io?dBTn}B0aRat&V)gNY*>)a*3?&KQSXPzt$~n_A@y!!9(9o>O<0`B2P3k-O4;%jSBKq$D^YpUIe*oN7;ay~)K@L@UGTHEv z0DQ5_kim2N>dOi>Wh}_ogN!ZBhBpG{HP41OUj)AfIIdOs+kn{;ZTkIy*%#XIKLE4k z+OUe40Ah{~B0NB_dD{e?0Jm-cr%6&#c6mR*9b3^41k8%&MOioVCeTsMCf$UI$v)Bi z(?rTQM^2=3$+-aSXrvpyxp)9l?T(4%6y?a!v!>71%V*7+K1HM-Dd}bpu(aFr@2|#v#o_x(8_?l0aIEv<2x|q`gRok&Yw%f)w6QQ{s`*kUApeBlSkQ z3h8E~3Z#3GYWryg%5r=>jPxW@Bhmo~vLE>eNS`5{K)Q4u-Bn5HQOX*_3K|~7z=WD5 zxzr&`*5HMm;RMrLMQei~}MMXpf{C#VV#|!9w_W%3*{(+vEs_L$;uCA`G zK3>y-p!+`#TJk4{MAb|0?E3d--<7M+(OCjakx`1|Ad;hFV&s^Z$mm!{Y@9=qrAU0p zk&*F{j!42^q!gEsAj`5tb|AMyigfTh*7#5Oc1W>_ibIZdBsx&a5$TZ0kt8P)B}65l z8t@}ZPKk_iL`8AY$ZGJE9jTF#26vJuEuQcx%c+nlmLlWQ<2VO!Qo9U%#e)mscDv6)spr?-8SRh% zcRoam=`ZwGT1w04Gg?Vc(=)VzcF`O30qv&`>1+CtexUR86a7qQ=zDZ=_)mMBXKA_Y zplZ6C?rmR#KS@)kim5@pX_>Df2-<8AO~Eud+A21L zbV>Vw%XvtZwJsHz1{0NBIy$AA?iJClSP>E$5mwXM4CZOtUXdQ!IbY6kgnFFHV_ecJ zsm>DFtx8&Gg-Up2%Tlo{sgg=LvQ#3Av7x2ZD3*t2(sA)hXl}^fFTqNATbv9X?Pq&b z3<~r5q0?ekSO-lbx!P)!GSw?tqH#5vQ;SpHn}^Ef{;HHMhk9vH1Zvb2e*>aik|bKg zE(DL0p`Lrip70D)YKLEH$A6OAcQL$=9JY6!+3L;Ws)*fTrwz!X?GuTS-7;+2viaPc zM_Cn;OS7@Ze3K)ca<~oF-Y4!*)bK{mt9dkl^-8XveY5Xuqc5|vfVWKD09qWRbc$EW& z4Dk#clq@KYp#lpq@`YMJ3{%_|5Y0fi1t<(eSU?m5VHObSf&lp}z^AB7Txi#WJ`p7u znPZO|VmdlW-O=NC6?uY@=`drVkzmMkH50VNV!69YMk^FsdKWV_TJ;-TL{mn3?3KAc z6(=(0q5cZBOGv$`KCxEKhC+9%llz+;;&iFOYLHfAkYBN^q468^hnA<+e~q_oBak63 zP&iTPDUID@G|T9-B<_zihxQk{5hW(P4RE1gb@rCGlR z{AJbwx#mr=tHZE{xz03gNsFlOm_fTmbI0CteT%r*u@E`BWe)`Ej_lt4 ze9bLlYj!b6-(`1`Yg$CdoO_FG9*r#la%(pB=o`O=JuN=W=@a!hw?lHNPS{_NvRrh? z?Jlp_C2q}~uG+51>Ux9?ob7!)83O--K7#T?wF04k9OHfR-c69%!!F3NS0 zOT~Z%(JhPVMM*Gz*+VW3^%cu2>cpC&5n*;KFY0N@loaENgug@eZ?g_G$yFq1n`btxwp!OqiognAtqYe3hD!jcMlU#u<} z6n(#Glkl&qoGrUSJ5#b+-uR|iR$4?Ki`}Ii-7T#1VmS#T_kOXuG!N*U9&ONd7P_uS zM>-)IdlVzfXFWQ~wY!DVli6I|v!lFnx0u@kUOt;x_j}J=xIRLUx|Je z=%y>tUje=MN_0V=boYrX(X;y8iHY>{J_Q}WVnxAxjQLiTlp{uGVidSN4jv5XFktIM zzrJ3$t?_+_(LwQC-+WB|AN3vRZdqisCJv>WMQ*=hIxoub{6MVe=b>8hazFTs1!7a* zJR`qHKdDft7&D@d&ijsz_?i$r>>c$Qy(gB8UhbcqPKttSv-~sUNip`?Br&4@lk(z) z;`{ztiKc+CBBuwQfsGDwLcp$C2RbTl(ne=3W4Bc-cY!4tUb9#(#b;)P*39D*pIs`H zkHsD>9}UVF884y+WXId%Sk~T=`~z7GA21LGyktNgofTUL>ekG1YOwNvm^0{imt_xD!@%a?pcj7?|HRd= zb$-*RwyPv99KaK7OIz@bw%{)D@ZgTLOT0X|pnByqkXNy$6G$7TwZ#MjbN*@0vz!Mj zoQoU$7aOH)R~(8wCdSUgr8c(V90ttRHW(vMY#WrB$@k}LIuXQsddOhfDZUvpBIM-f zEWklAXlU1v^Vau0Lvym5-eJ5Mjld`~wa~#4FxhNb&5@)5^}AmE^n>Rf+oE14b`NbI zvhoX)$(f-&(>5C4hYa^aS}8meM>2TCDO<$QVQR=$GlQ5tERME_g~M*~hmNh{i(wu= zbWp?%_f|K&&8%%D!v3_THkdGA_6K4#JK8;Hw8oC@Z0usAb{pHmt<<_`=Zw}`Sfi_2 zOrGL};p4;VxG^rop29n#2r}PsZRv=OJc($(;4g${3FR0Y>YxaA{MmB7F~g81Y1PFh zpi+dBDS^czI0Gh$hC1={wQ1ES&Ow%9c?qeKJ&IQF_Y$hm7XJvEV$GRmT^@gF2%;i} zHmeS-z%N$>W*LooO6yrJNXYY@A>)916v)kUi57%y}W4m~?$! z)YgSu`E#7l6Xf=Y`s@3XSA2Z^b*YFHrB2c$?GWNNOH^<=EmbIo#mE~H(-1tO&680o z8nt?YHN=}1n`klbhEb%5cW>w}vTltQ2{$H$M>4miXi9`AzOkSm0!*Xz7Ry7XpxMxn zH*`=$q#80nt$WH7rb;>TGV+9|gnBO37%A4@IDovq&u*Me^87{O<}n3ySS%VdU9MZ? z`*FMx7C;(`be0@`um-v%^N8I!==@d5tg2j{P?`!t4+{*<`*DtI9gimtsfR zMviwY$B#-s+5+t;O;%Q9;>wcvk*1s!hsXCv0Gu(Q4`HU6(2Gv`9-T0k=#+>mzX?&s zjtf>teh5i-Ja2_mHS02G_z=RwG1u3A5TB~hs^>uGwnmB zYNaT@?{*L7;Ip@njj)36P;IY-==-+Gy*7794{0M}s*9x?$Maa$}v?P|+==wH}mW zIDLOK;KrxBLJ{JJiV^7OK~tBcnF}?<%xvr^izvZZ4m)@`{LR!-L<+gn^3k?2)5@$> zo#b|K(BwqtDxZqCrsYN+J;^dyD*v7MCM2c1lbInRo~qI)ShLI<8)m?AMMP|Pb>vXQ z;prXI*KvDz?TqzT#Q-XSM22^c!|ROj&d6_Xw)nW&p~XDZF{?=!rpLCUsJ26b?q<*M zI1dv!^=;vl6chu8cS`(yMnd%p?$r{B7q?1JK$q3P6OSU{)h;5{15g~c)^l}?h?^(dx%%hGuroZF$r_mp@mVDsYUY;GFLsZK5l z8bgxi=m`PHzw?597jf%MFMTblXZACetAm|OiAZ?1CJ1Lb(tjEENI9@y%>$o@OuE(Ns+8sR*nLc^P z=#YjTT+kC5LBe353*8UD{PQ(L+eOSh_p0@*4%33I zC|p4wqoW#_pd8l^D3t2^cmW?gSnRy77ljC_?T%-0Z3zX78MPUBK2+=J z91Igw!`b;b)L`^AiaB{x(-X|LEB7~sEOw|R8O9nc4%K>G!7hzFyt@QAY(Zv*Ss)sE zu92h&w8N}F6k(xS;SCW(7X0qA1-)3kpr1T%o;b1~38UuRf_+(byBz*@xnd-6YGcoO z#~|uQwfgNrG%oI^bx^v7eHU#WZ+ub3RAqtwVG#Y{*D0G)DLr+?<0zvnG%W&!YDlwD z;<^W?%MFih8mJt1E3FOqAB{!t92R`Zy@Uxa?2ApSbHTu*0h_=7wDyOS z`wlIwE^%NU81TT0OPkNGS@q`iB@W4zfE;W+p;lEeDy}VzyVdvtVD#}(_D5=%<+NNc zilYnPrS~71LD!^0P&ge70%J!9gTOC?qoww*PNPrlml z@tea;(H?0%yI|?b##0#(O-%di3T%#{Q?U2dYf16Q6=t~&IApOm;ZI9by$-Xj=<@ot z5RpfRLp2>&JiHa7{eno-0gF_fUaKx_F#4++_w9BZP{BPn+?~zTzs{xQr`TMZ~P-p3cw|Z`<9ng*F1!-&}3ybHzdUvEYKZ$ zU#vh3nzIe9S+a;L3}Q$dVhvfu%?2^DEz#Txm9&!vG1VYOqD~dFx0a26n_m*M^?a8x zBt1^twJ50{Md2Y27VGRX`Q5Bl)pS8dql?P&i{1|)B+eF36x52;1s-AdOts2yA|0)*LFBTj? z)8&{E=spJsEha&HxoV@_x>`KCx=`M;TD-mb0fv6Rrn9Ze!XVQuTCN2=KA0LDuFFEJ;k0#Y78R{oLj`t_1DoEb~l%A;^;< zz5Mwm@D{FK$}b3BcGVMu&cB%y%c&i5H$2B{!`T!B6(a%3$D= zg>vgUap#j|2~Ts6Ll;5tm|BxLJ1S3$ub*s(5fb@Sk?*;6r%67xPSme|h+kcvD2z1x zZ!718>$o7UeQG#;Cssd|N#BaspXw6V%)yKnq-^6p#Uh-volp2e8s?L{;$`vR)06r2 z{nME-JDQNsZ(vSs<;x=PnT|}o;hACb%2&kNXAa9vYs9<_@%&PrHC~%csPD6x11>TH zBe+K$SiVNhzO;hvDMjF$i*Gob!ikhOg6obld4 z+SW~s*zQmo4dsxzAiDTGW3#olef(Y{Yo}h|k4>`DC^l?V<$X~4NF*VVc{X8MOj6nZkm+Atk zPGys?u<>zO-nT(SY|auFp6?0+D)0@Jmu)l<^}ci<{^1(|Q*&<~QDTK99L;Gh8v}$e z_tdmJu(Zc2YT@~gD-laJ=h8ay%I0>(jcmmnM*Ekq=Jq8-O5#uw_|OV-MZ%sI+u|^` zbt3A8u?VZDzo3v@`;xEv#bqS#cuK@KbxUb_oyD*Oi$)R5ej4%Q<|}J@k?19n^zu+@ z^i6oV7sb59J&VmD4o@MPti0rFdx;vX^$qvXbr1G`p$e%hVC2|YldtvhaF)U0oqEe6h*_%YZKRSi4<a8X;)xEVOh(JQub^beMO!tOI^Vk-H!egJKy?)So6=gX>Fr`OS!EmK-*#T zQcBxi`d?y2#s_EZkj|I(ujzysVbT6R>GrB72|+5XCLKHb?jUn0?OP&_?a$6@E9I5_ z&$>Cr!xU%uB?k&=t(bbCSpL^)@%Vu*C3|>O7t4*@f#@h~+hCk>i8E#i9&FfmAPf>G zpH6J%j3^uaVeh0SCK`mjk=Dvd4R+NI^!?$(8z?ov1*uMW9zB^N@mCLW}~il2|Dq>G=9cu4o99ql29*>38b_`@d+^2U`S^wWdT_wi3N@eDtf znP534hgOHhF#cP<>8fMb7n!~Z(llcy+p_l*fXC!ZAbT#etfaaP&((UYeTM0x{3ZRM zS#p(#Jf4tmyD{vcZ6!}GGOxsn+<{FZTuthz?&CRcjlASO(U`C0FWDvoV6P#$u}9 zT@>&xUqcYq*A8uO@UNA^O7e7wN%gynE&-*^gaQy& zpOxcF{BDy>Ys4Ghuc$U#YI}yw_UsfG+-*O2@x1ScTzT^n-=-g~vUTt4`=itP?k8rQ2osBc>er!d zTRQof*)^K%#Wz1ChU~Bgebmn>81%V6_nT;rSniwVaC^kcShD?}I$MKLN&a%3e%L`h=$xM9r0!TH=p&spJn}xS2@^Kk8|M>w7jihE1U%?*70G1F zX9l=<$J;^J$}vQ#)4PP?9`3umk$Am6JdB3Zd-|hc=+gJ~x5MauYscBh`>kFU4m)^n zo_NsPak-0%}zPaXZkJoEUTKgviRU7TYCN?npQ0Fn#(+Rd4>_@(F~4Z z)?L(d+%!O5c}|~>H+`%(y6NhIGkh7aSe}e-Sin4s*d?p-Io35XT4QM#Z{Gu<)@ssJ8?% z@R1fi#%|J2*+ZEK;Ia6TSLKa*s_8D#lYJQ2;w!mfq(c!b4@x{P8K7l zIjAAfb08Qtp|?a~Abp_!JBp^#3H>?+8@jVsCkCVqQs_{LwtIrlftev2kLWtGRD2noHW_mNd%n z=(q0>ny}46e+Lv?ulC5B2S%9 z>P7#dnfk7HLv`*1%EJ#AjQ(hEv>LxT@H$NeAkL42=5fXKYu1M`RbkB<$GaS=Ge=Is znYg(^=b1?HaVFsLYl0x_UcLi|n|!LEMVY58)-f~uB23R|orQ%%)qwH;l>Tx8^`+1C z9}}pk`pX}nD~)e>IV&`snJW#f4-Y~DExV@_3(NT0pC`6LTM>{4XE6SAaPm8TTqVL> zeYxXj1LG5SzvIVVQ;sCV^KVL|w3P3RIzmusT!m(=Fibg=jr#FK^2DMd^5AsB1ARj~ zIE?s7Z*( zJThebf2@?J-VBH^vV(DWDEH_4CD6yz49b!>{-{5kK@+VK@csHP8C2S9qbZ|PMVQ1xS+>S5 zhAzxr8nFNHg|B}&RsX>v>!ftk>;GD`<-7#gF%=R?lO}ILHgd zeF`(!!)P8@pl_!VAe4dDwg<3Hm zMM&s+RsRs4Dq-uT<`_rBaga#cy4*uI3}~+9{z=ms_-#*E+Gzvp(GCV;-CPPv)Zo`@X0&8=qyYsMFuirta<==ag5@DVS4LHfhRK{kEHFgg!2Z zf_lRax=D&O4(~^u5>}&^;J*Uy5C9|2kdgrV|MaCfG>pdShjOT2SLV9(FfgV&Pamqx>LJGBDuD>0UTYoo~3Wns$iWCL#AKewnh4=I+ zbLLE$al1NcO8K;j>TS|+i@qUKkx-X3WyT$4)22*RrA{F5q>os1Y8Q-ON2jMLDKGjFu;MJkk;0H3+0vtcZlUV^AquYJNzyh8Z^J(U=AO1;n42Mn4bKM53bElk z0FMcP@4GC2Ip9Ek!vK3F3Ch?K-IWFgWi>@8MxQ(lCP7Fx9FC0KDaM~ZyBH?bq%SR| zLcDesQ;BA0ECeskXv5P0b4zXbQ@|qw;I9D>WmxZ10&XYt*(H=0)UiuP>4g472`0-E z`sXF+>kWExDWH$_v8B|$-Ff^lIR&M9-Xckjr3$_<0u<92RYhN2N?z)zZ!4ve^jm>e zLB1RB7`$;JYVGxt77eUbVhsr>h6H6M@g{P26!d$O~`!^d{HbJ~>gm1UKGDwr^{vbrmmZ0&B{uUj?b~mYP+MM#L%DLtEwrNEW zkQHRZo%L&bQLmhnF{TnO;_U^wvTVlfh3PH5seZ`8IK`0cYDmaB4}`M=;2yz} z!Oy1Oei=Mj|9c-QNWL~+ks?uS9Nw(h33zjVZP0)0LxVlb0J4`_gEu>}op`fj+k-b4 zB3oHs%80hJod=p5wLxFnm)eKrCz#p{(_igN1u-`P%mS5_#{>SRge@2Q((^&-X=w^J zZX{_m62EQuR(vPuo??niE1xr=Y*snC?$)_~26iG!Fq`p6?Kwx1d;FB!&CZpiWm`Oh z=q-odGLqc-xoaq~V`;bYuEkwSCr&CWF7GnFsHnI{x592kMLo(3Cl;0#O)4)dD(g|$ zwX0q{guLl1;Y3-KV0cR&(FJWLp8i8=NKE;RJEm05oH4z8MpXg6A(uXR2wm05_P^UL zXJEsx174J2+ByGa|a#y6X}4cFp3CjefKZ-0LMz)^b%-^N1YA#1k5vv4G#p&GlLBe0^Eap$VSuyksAO% zc^QL80Q2xR^6TFZr;b_cFwbH@OPA)`s`A^*D=`y8D>hBnMo?ndet_oKC>uesL(c)^ zv0;~-Q&q_mxfGYBNU$7f1?W7p&Ydx(O2s73P9z^R#*La~OkxJMf0q7_5tPv92GH0> z=9JH#TRvk#`82>h)~1)ulJ3SgrgW(sCSHec9;z1K$|+?ts+Q@|BPlVt0VEDCs^-p` zR*pIi8sKJK137*_$Xvr*o?`7k4uLZC8Vr@N0l*DJx=Md{BwhDk51OH)s9pcNJ1EjH zlr2Q_3`?(^JEv+w!GAgMx)tcx^dYr+Ke&tnmq<95IJ^U4PXH`mMrWAm1^KY`Ebvco*Q<0Qx(Cd1klszXh1xoDKgI za8Da%FD)ZBU}^0PZos_);0}NTE5Ig~p)NarU%)&o8Ss`q%ryZ zt)2xud%ydUxB)g?3xQaJffWx1%#C96;jw?sW$+NdLy>p{vWwz8!;#pGyCU%)6lZ8| ze3r6YAk>d{N_ zN;lwRJW?f69nvbK2Bd!=y@m9Fo;8J@s&@8)oJeU%osfDX4MQ4>Gy~}_r2CKr(rTmz zq!*B0Luy6ZkMudx&q!f?5mX`NAr&L_MH-BB9nx5&Dx?~u#Yn4>p6si1lQ!d{8R;FQ zPmoR_okI%hhejdABefke#ytFd1*ONgXy&B0O4aalmGw# From 77b47d2943300e7c61cd0c5eee32b4606caf6f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Mon, 29 Jan 2024 13:16:13 +0100 Subject: [PATCH 30/36] Nit fixes --- chain/rosetta-rpc/src/adapters/mod.rs | 2 +- core/primitives-core/src/account.rs | 92 +++++++++++++-------------- core/primitives/src/test_utils.rs | 2 +- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index cddd2038420..3de05c6abec 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -353,7 +353,7 @@ impl From for Vec { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - // Note(jakmeier): Both refundable and non-refundable transfers are considered as available balance. + // Both refundable and non-refundable transfers are considered as available balance. // TODO(nonrefundable) Merge with the arm above on stabilization. near_primitives::transaction::Action::NonrefundableStorageTransfer(action) => { let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 6f0ee7530f7..33ab838bcb5 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -60,11 +60,7 @@ pub struct Account { code_hash: CryptoHash, /// Storage used by the given account, includes account id, this struct, access keys and other data. storage_usage: StorageUsage, - /// Version of Account in re migrations and similar - /// - /// Note(jakmeier): Why does this exist? We only have one version right now - /// and the code doesn't allow adding a new version at all since this field - /// is not included in the merklized state... + /// Version of Account in re migrations and similar. #[serde(default)] version: AccountVersion, } @@ -85,8 +81,10 @@ impl Account { code_hash: CryptoHash, storage_usage: StorageUsage, ) -> Self { - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - assert_eq!(nonrefundable, 0); + let version = AccountVersion::default(); + if version == AccountVersion::V1 { + assert_eq!(nonrefundable, 0); + } Account { amount, locked, @@ -94,7 +92,7 @@ impl Account { nonrefundable, code_hash, storage_usage, - version: AccountVersion::default(), + version, } } @@ -166,8 +164,8 @@ impl Account { } } -/// Note(jakmeier): Even though this is called "legacy", it looks like this is -/// the one and only serialization format of Accounts currently in use. +/// These accounts are serialized in merklized state. +/// We keep old accounts in the old format to avoid migration of the MPT. #[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] #[cfg_attr( not(feature = "protocol_feature_nonrefundable_transfer_nep491"), @@ -180,6 +178,17 @@ struct LegacyAccount { storage_usage: StorageUsage, } +/// We only allow nonrefundable storage on new accounts (see `LegacyAccount`). +#[derive(BorshSerialize, BorshDeserialize)] +struct AccountV2 { + amount: Balance, + locked: Balance, + code_hash: CryptoHash, + storage_usage: StorageUsage, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + nonrefundable: Balance, +} + /// We need custom serde deserialization in order to parse mainnet genesis accounts (LegacyAccounts) /// as accounts V1. This preserves the mainnet genesis hash. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] @@ -267,22 +276,16 @@ impl BorshDeserialize for Account { format!("Expected account version 2 or higher, got {:?}", version), )); } - - let amount = u128::deserialize_reader(rd)?; - let locked = u128::deserialize_reader(rd)?; - let code_hash = CryptoHash::deserialize_reader(rd)?; - let storage_usage = StorageUsage::deserialize_reader(rd)?; - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let nonrefundable = u128::deserialize_reader(rd)?; + let account = AccountV2::deserialize_reader(rd)?; Ok(Account { - amount, - locked, - code_hash, - storage_usage, - version, + amount: account.amount, + locked: account.locked, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable, + nonrefundable: account.nonrefundable, + code_hash: account.code_hash, + storage_usage: account.storage_usage, + version, }) } else { // Account v1 @@ -320,23 +323,19 @@ impl BorshSerialize for Account { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] { match self.version { - // Note(jakmeier): It might be tempting to lazily convert old V1 to V2 + // It might be tempting to lazily convert old V1 to V2 // while serializing. But that would break the borsh assumptions // of unique binary representation. - AccountVersion::V1 => legacy_account.serialize(writer), - // Note(jakmeier): These accounts are serialized in merklized state. - // I would really like to avoid migration of the MPT. - // This here would keep old accounts in the old format - // and only allow nonrefundable storage on new accounts. - AccountVersion::V2 => { - #[derive(BorshSerialize)] - struct AccountV2 { - amount: Balance, - locked: Balance, - code_hash: CryptoHash, - storage_usage: StorageUsage, - nonrefundable: Balance, + AccountVersion::V1 => { + if self.nonrefundable > 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Trying to serialize V1 account with nonrefundable amount"), + )); } + legacy_account.serialize(writer) + } + AccountVersion::V2 => { let account = AccountV2 { amount: self.amount(), locked: self.locked(), @@ -518,6 +517,9 @@ mod tests { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + /// It is impossible to construct V1 account with nonrefundable amount greater than 0. + /// So the situation in this test is theoretical. + /// /// Serialization of account V1 with non-refundable amount greater than 0 would pass without an error, /// but an error would be raised on deserialization of such invalid data. #[test] @@ -558,8 +560,10 @@ mod tests { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - /// That must not happen, but if a V1 account had nonrefundable amount greater than zero, - /// it would be truncated during Borsh serialization. + /// It is impossible to construct V1 account with nonrefundable amount greater than 0. + /// So the situation in this test is theoretical. + /// + /// If a V1 account had nonrefundable amount greater than zero, it would fail during Borsh serialization. #[test] fn test_account_v1_borsh_serialization_invalid_data() { let account = Account { @@ -570,14 +574,8 @@ mod tests { storage_usage: 100, version: AccountVersion::V1, }; - let serialized_account = borsh::to_vec(&account).unwrap(); - assert_eq!( - &hash(&serialized_account).to_string(), - "EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ" - ); - let deserialized_account = - ::deserialize(&mut &serialized_account[..]).unwrap(); - assert_eq!(deserialized_account.nonrefundable, 0); + let serialized_account = borsh::to_vec(&account); + assert!(serialized_account.is_err()); } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 02ebf8161bd..c646ce248e8 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -682,7 +682,7 @@ impl FinalExecutionOutcomeView { } } - /// Calculates how much NEAR was burn for gas, after refunds. + /// Calculates how much NEAR was burnt for gas, after refunds. pub fn gas_cost(&self) -> Balance { self.transaction_outcome.outcome.tokens_burnt + self.receipts_outcome.iter().map(|r| r.outcome.tokens_burnt).sum::() From c43bee33cf7c5ceb163fa555f6f758ab27a38b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Mon, 29 Jan 2024 18:05:42 +0100 Subject: [PATCH 31/36] Disable multiple transfers on implicit account creation --- core/primitives/src/action/mod.rs | 3 +-- .../tests/client/features/nonrefundable_transfer.rs | 12 ++++++------ runtime/runtime/src/actions.rs | 4 ++-- runtime/runtime/src/lib.rs | 12 ------------ 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index e97edd03de4..91f6f3f1465 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -196,8 +196,7 @@ pub enum Action { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] /// Makes a non-refundable transfer for storage allowance. /// Only possible during new account creation. - /// For implicit account creation, it has to be the first action - /// in the receipt. Following regular transfers are allowed in the same receipt. + /// For implicit account creation, it has to be the only action in the receipt. NonrefundableStorageTransfer(NonrefundableStorageTransferAction), } diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 197cda0e01a..2d92fe9ccba 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -391,15 +391,15 @@ fn non_refundable_transfer_create_near_implicit_account() { deploy_contract: false, }, ); - if transfers.nonrefundable_transfer_first { + if transfers.regular_amount == 0 { tx_result.unwrap().assert_success(); } else { - // Non-refundable transfer must be the first action if it appears in implicit account creation transaction. + // Non-refundable transfer must be the only action in an implicit account creation transaction. let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; assert!(matches!( status, ExecutionStatusView::Failure(TxExecutionError::ActionError( - ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + ActionError { kind: ActionErrorKind::AccountDoesNotExist { account_id }, .. } )) if *account_id == new_account_id, )); } @@ -424,15 +424,15 @@ fn non_refundable_transfer_create_eth_implicit_account() { deploy_contract: false, }, ); - if transfers.nonrefundable_transfer_first { + if transfers.regular_amount == 0 { tx_result.unwrap().assert_success(); } else { - // Non-refundable transfer must be the first action if it appears in implicit account creation transaction. + // Non-refundable transfer must be the only action in an implicit account creation transaction. let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; assert!(matches!( status, ExecutionStatusView::Failure(TxExecutionError::ActionError( - ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + ActionError { kind: ActionErrorKind::AccountDoesNotExist { account_id }, .. } )) if *account_id == new_account_id, )); } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 724cd9c1be0..4bc766b8914 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -1023,7 +1023,7 @@ pub(crate) fn check_account_existence( // the receipt being a `CreateAccount` action serves this // purpose. // For implicit accounts creation with non-refundable storage - // we require that this is the first action in the receipt. + // we require that this is the only action in the receipt. return Err(ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id: account_id.clone(), } @@ -1059,7 +1059,7 @@ fn check_transfer_to_nonexisting_account( { // OK. It's implicit account creation. // Notes: - // - Transfer actions have to be the only actions in the transaction to avoid + // - Transfer action has to be the only action in the transaction to avoid // abuse by hijacking this account with other public keys or contracts. // - Refunds don't automatically create accounts, because refunds are free and // we don't want some type of abuse. diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index b229f795d40..bddd7779bbd 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -345,21 +345,9 @@ impl Runtime { result.compute_usage = exec_fees; let account_id = &receipt.receiver_id; let is_refund = receipt.predecessor_id.is_system(); - - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] let is_the_only_action = actions.len() == 1; - - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let only_transfers = actions.iter().all(|action| { - matches!(action, Action::Transfer(_) | Action::NonrefundableStorageTransfer(_)) - }); - - #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] let implicit_account_creation_eligible = is_the_only_action && !is_refund; - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let implicit_account_creation_eligible = only_transfers && !is_refund; - let receipt_starts_with_create_account = matches!(actions.get(0), Some(Action::CreateAccount(_))); // Account validation From 69a2287f9fa2cab499db59cb4a3655ee7036cacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Tue, 30 Jan 2024 13:13:41 +0100 Subject: [PATCH 32/36] Use protocol version for Account::new() --- chain/chain/src/test_utils/kv_runtime.rs | 1 + chain/rosetta-rpc/src/lib.rs | 12 +++++++++-- core/chain-configs/src/genesis_validate.rs | 3 ++- core/primitives-core/src/account.rs | 21 +++++++++++++++---- core/primitives/src/test_utils.rs | 2 +- core/primitives/src/views.rs | 10 ++++++++- .../genesis-csv-to-json/src/csv_parser.rs | 10 ++++++++- genesis-tools/genesis-populate/src/lib.rs | 1 + .../tests/client/features/chunk_validation.rs | 9 +++++++- .../tests/client/features/in_memory_tries.rs | 9 +++++++- .../src/tests/runtime/state_viewer.rs | 4 ++-- nearcore/src/config.rs | 2 +- runtime/runtime/src/actions.rs | 11 ++++++++-- runtime/runtime/src/config.rs | 2 ++ runtime/runtime/src/lib.rs | 1 + .../runtime/tests/runtime_group_tools/mod.rs | 1 + test-utils/testlib/src/runtime_utils.rs | 5 +++-- tools/amend-genesis/src/lib.rs | 12 +++++++++-- tools/fork-network/src/cli.rs | 1 + 19 files changed, 96 insertions(+), 21 deletions(-) diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 89ed5bc0590..273a7a17a50 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -1248,6 +1248,7 @@ impl RuntimeAdapter for KeyValueRuntime { 0, CryptoHash::default(), 0, + PROTOCOL_VERSION, ) .into(), ), diff --git a/chain/rosetta-rpc/src/lib.rs b/chain/rosetta-rpc/src/lib.rs index 414ae3907aa..e8be28019ed 100644 --- a/chain/rosetta-rpc/src/lib.rs +++ b/chain/rosetta-rpc/src/lib.rs @@ -17,7 +17,7 @@ pub use config::RosettaRpcConfig; use near_chain_configs::Genesis; use near_client::{ClientActor, ViewClientActor}; use near_o11y::WithSpanContextExt; -use near_primitives::borsh::BorshDeserialize; +use near_primitives::{borsh::BorshDeserialize, version::PROTOCOL_VERSION}; mod adapters; mod config; @@ -368,7 +368,15 @@ async fn account_balance( Err(crate::errors::ErrorKind::NotFound(_)) => ( block.header.hash, block.header.height, - near_primitives::account::Account::new(0, 0, 0, Default::default(), 0).into(), + near_primitives::account::Account::new( + 0, + 0, + 0, + Default::default(), + 0, + PROTOCOL_VERSION, + ) + .into(), ), Err(err) => return Err(err.into()), }; diff --git a/core/chain-configs/src/genesis_validate.rs b/core/chain-configs/src/genesis_validate.rs index 39535eca7a8..8888771cced 100644 --- a/core/chain-configs/src/genesis_validate.rs +++ b/core/chain-configs/src/genesis_validate.rs @@ -200,11 +200,12 @@ mod test { use near_crypto::{KeyType, PublicKey}; use near_primitives::account::{AccessKey, Account}; use near_primitives::types::AccountInfo; + use near_primitives::version::PROTOCOL_VERSION; const VALID_ED25519_RISTRETTO_KEY: &str = "ed25519:KuTCtARNzxZQ3YvXDeLjx83FDqxv2SdQTSbiq876zR7"; fn create_account() -> Account { - Account::new(100, 10, 0, Default::default(), 0) + Account::new(100, 10, 0, Default::default(), 0, PROTOCOL_VERSION) } #[test] diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 33ab838bcb5..03770764c3c 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -1,6 +1,8 @@ +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +use crate::checked_feature; use crate::hash::CryptoHash; use crate::serialize::dec_format; -use crate::types::{Balance, Nonce, StorageUsage}; +use crate::types::{Balance, Nonce, ProtocolVersion, StorageUsage}; use borsh::{BorshDeserialize, BorshSerialize}; pub use near_account_id as id; use std::io; @@ -80,9 +82,20 @@ impl Account { nonrefundable: Balance, code_hash: CryptoHash, storage_usage: StorageUsage, + #[cfg_attr(not(feature = "protocol_feature_nonrefundable_transfer_nep491"), allow(unused))] + protocol_version: ProtocolVersion, ) -> Self { - let version = AccountVersion::default(); - if version == AccountVersion::V1 { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let account_version = AccountVersion::V1; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let account_version = if checked_feature!("stable", NonRefundableBalance, protocol_version) + { + AccountVersion::V2 + } else { + AccountVersion::V1 + }; + if account_version == AccountVersion::V1 { assert_eq!(nonrefundable, 0); } Account { @@ -92,7 +105,7 @@ impl Account { nonrefundable, code_hash, storage_usage, - version, + version: account_version, } } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index c646ce248e8..634c41d165d 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -27,7 +27,7 @@ use crate::version::PROTOCOL_VERSION; use crate::views::{ExecutionStatusView, FinalExecutionOutcomeView, FinalExecutionStatus}; pub fn account_new(amount: Balance, code_hash: CryptoHash) -> Account { - Account::new(amount, 0, 0, code_hash, std::mem::size_of::() as u64) + Account::new(amount, 0, 0, code_hash, std::mem::size_of::() as u64, PROTOCOL_VERSION) } impl Transaction { diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index cd161898798..bbad977da90 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -43,6 +43,7 @@ use chrono::DateTime; use near_crypto::{PublicKey, Signature}; use near_fmt::{AbbrBytes, Slice}; use near_parameters::{ActionCosts, ExtCosts}; +use near_primitives_core::version::PROTOCOL_VERSION; use serde_with::base64::Base64; use serde_with::serde_as; use std::collections::HashMap; @@ -126,7 +127,14 @@ impl From<&AccountView> for Account { let nonrefundable = view.nonrefundable; #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] let nonrefundable = 0; - Account::new(view.amount, view.locked, nonrefundable, view.code_hash, view.storage_usage) + Account::new( + view.amount, + view.locked, + nonrefundable, + view.code_hash, + view.storage_usage, + PROTOCOL_VERSION, + ) } } diff --git a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs index 26980b1d126..4a16c517e59 100644 --- a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs +++ b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs @@ -10,6 +10,7 @@ use near_primitives::receipt::{ActionReceipt, Receipt, ReceiptEnum}; use near_primitives::state_record::StateRecord; use near_primitives::transaction::{Action, FunctionCallAction}; use near_primitives::types::{AccountId, AccountInfo, Balance, Gas}; +use near_primitives::version::PROTOCOL_VERSION; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -187,7 +188,14 @@ fn account_records(row: &Row, gas_price: Balance) -> Vec { let mut res = vec![StateRecord::Account { account_id: row.account_id.clone(), - account: Account::new(row.amount, row.validator_stake, 0, smart_contract_hash, 0), + account: Account::new( + row.amount, + row.validator_stake, + 0, + smart_contract_hash, + 0, + PROTOCOL_VERSION, + ), }]; // Add restricted access keys. diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index 0b4c3ec0885..3c2d7651152 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -283,6 +283,7 @@ impl GenesisBuilder { 0, self.additional_accounts_code_hash, 0, + self.genesis.config.protocol_version, ); set_account(&mut state_update, account_id.clone(), &account); let account_record = StateRecord::Account { account_id: account_id.clone(), account }; diff --git a/integration-tests/src/tests/client/features/chunk_validation.rs b/integration-tests/src/tests/client/features/chunk_validation.rs index 410f0aa27fc..5e7d7b139a8 100644 --- a/integration-tests/src/tests/client/features/chunk_validation.rs +++ b/integration-tests/src/tests/client/features/chunk_validation.rs @@ -106,7 +106,14 @@ fn run_chunk_validation_test(seed: u64, prob_missing_chunk: f64) { let staked = if i < num_validators { validator_stake } else { 0 }; records.push(StateRecord::Account { account_id: account.clone(), - account: Account::new(initial_balance, staked, 0, CryptoHash::default(), 0), + account: Account::new( + initial_balance, + staked, + 0, + CryptoHash::default(), + 0, + PROTOCOL_VERSION, + ), }); records.push(StateRecord::AccessKey { account_id: account.clone(), diff --git a/integration-tests/src/tests/client/features/in_memory_tries.rs b/integration-tests/src/tests/client/features/in_memory_tries.rs index 20d8ef72796..6a1ab3e9228 100644 --- a/integration-tests/src/tests/client/features/in_memory_tries.rs +++ b/integration-tests/src/tests/client/features/in_memory_tries.rs @@ -108,7 +108,14 @@ fn test_in_memory_trie_node_consistency() { let staked = if i < 2 { validator_stake } else { 0 }; records.push(StateRecord::Account { account_id: account.clone(), - account: Account::new(initial_balance, staked, 0, CryptoHash::default(), 0), + account: Account::new( + initial_balance, + staked, + 0, + CryptoHash::default(), + 0, + PROTOCOL_VERSION, + ), }); records.push(StateRecord::AccessKey { account_id: account.clone(), diff --git a/integration-tests/src/tests/runtime/state_viewer.rs b/integration-tests/src/tests/runtime/state_viewer.rs index 19f43a378f9..3179d1af3a6 100644 --- a/integration-tests/src/tests/runtime/state_viewer.rs +++ b/integration-tests/src/tests/runtime/state_viewer.rs @@ -360,7 +360,7 @@ fn test_view_state_too_large() { set_account( &mut state_update, alice_account(), - &Account::new(0, 0, 0, CryptoHash::default(), 50_001), + &Account::new(0, 0, 0, CryptoHash::default(), 50_001, PROTOCOL_VERSION), ); let trie_viewer = TrieViewer::new(Some(50_000), None); let result = trie_viewer.view_state(&state_update, &alice_account(), b"", false); @@ -375,7 +375,7 @@ fn test_view_state_with_large_contract() { set_account( &mut state_update, alice_account(), - &Account::new(0, 0, 0, sha256(&contract_code), 50_001), + &Account::new(0, 0, 0, sha256(&contract_code), 50_001, PROTOCOL_VERSION), ); state_update.set(TrieKey::ContractCode { account_id: alice_account() }, contract_code); let trie_viewer = TrieViewer::new(Some(50_000), None); diff --git a/nearcore/src/config.rs b/nearcore/src/config.rs index 872d233770c..c42550810ed 100644 --- a/nearcore/src/config.rs +++ b/nearcore/src/config.rs @@ -755,7 +755,7 @@ fn add_account_with_key( ) { records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(amount, staked, 0, code_hash, 0), + account: Account::new(amount, staked, 0, code_hash, 0, PROTOCOL_VERSION), }); records.push(StateRecord::AccessKey { account_id, diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 4bc766b8914..71e45594bab 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -426,6 +426,7 @@ pub(crate) fn action_create_account( account_id: &AccountId, predecessor_id: &AccountId, result: &mut ActionResult, + protocol_version: ProtocolVersion, ) { if account_id.is_top_level() { if account_id.len() < account_creation_config.min_allowed_top_level_account_length as usize @@ -461,6 +462,7 @@ pub(crate) fn action_create_account( 0, CryptoHash::default(), fee_config.storage_usage_config.num_bytes_account, + protocol_version, )); } @@ -508,6 +510,7 @@ pub(crate) fn action_implicit_account_creation_transfer( + public_key.len() as u64 + borsh::object_length(&access_key).unwrap() as u64 + fee_config.storage_usage_config.num_extra_bytes_record, + current_protocol_version, )); set_access_key(state_update, account_id.clone(), public_key, &access_key); @@ -531,6 +534,7 @@ pub(crate) fn action_implicit_account_creation_transfer( nonrefundable_balance, *magic_bytes.hash(), storage_usage, + current_protocol_version, )); set_code(state_update, account_id.clone(), &magic_bytes); @@ -1084,6 +1088,7 @@ mod tests { use near_primitives::transaction::CreateAccountAction; use near_primitives::trie_key::TrieKey; use near_primitives::types::{EpochId, StateChangeCause}; + use near_primitives_core::version::PROTOCOL_VERSION; use near_store::set_account; use near_store::test_utils::TestTriesBuilder; use std::sync::Arc; @@ -1107,6 +1112,7 @@ mod tests { &account_id, &predecessor_id, &mut action_result, + PROTOCOL_VERSION, ); if action_result.result.is_ok() { assert!(account.is_some()); @@ -1192,7 +1198,8 @@ mod tests { storage_usage: u64, state_update: &mut TrieUpdate, ) -> ActionResult { - let mut account = Some(Account::new(100, 0, 0, *code_hash, storage_usage)); + let mut account = + Some(Account::new(100, 0, 0, *code_hash, storage_usage, PROTOCOL_VERSION)); let mut actor_id = account_id.clone(); let mut action_result = ActionResult::default(); let receipt = Receipt::new_balance_refund(&"alice.near".parse().unwrap(), 0); @@ -1330,7 +1337,7 @@ mod tests { let tries = TestTriesBuilder::new().build(); let mut state_update = tries.new_trie_update(ShardUId::single_shard(), CryptoHash::default()); - let account = Account::new(100, 0, 0, CryptoHash::default(), 100); + let account = Account::new(100, 0, 0, CryptoHash::default(), 100, PROTOCOL_VERSION); set_account(&mut state_update, account_id.clone(), &account); set_access_key(&mut state_update, account_id.clone(), public_key.clone(), access_key); diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 8b1e3c72278..ed8a6b3364c 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -103,6 +103,8 @@ pub fn total_send_fees( ) } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + // TODO(nonrefundable) Before stabilizing, consider using separate gas cost parameters + // for non-refundable and regular transfers. NonrefundableStorageTransfer(_) => { // Account for implicit account creation transfer_send_fee( diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index bddd7779bbd..5fad656da2b 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -378,6 +378,7 @@ impl Runtime { &receipt.receiver_id, &receipt.predecessor_id, &mut result, + apply_state.current_protocol_version, ); } Action::DeployContract(deploy_contract) => { diff --git a/runtime/runtime/tests/runtime_group_tools/mod.rs b/runtime/runtime/tests/runtime_group_tools/mod.rs index bb9841e15c4..8b64717adb1 100644 --- a/runtime/runtime/tests/runtime_group_tools/mod.rs +++ b/runtime/runtime/tests/runtime_group_tools/mod.rs @@ -226,6 +226,7 @@ impl RuntimeGroup { 0, code_hash, 0, + PROTOCOL_VERSION, ), }); state_records.push(StateRecord::AccessKey { diff --git a/test-utils/testlib/src/runtime_utils.rs b/test-utils/testlib/src/runtime_utils.rs index 8c9c0e13340..7dcc5153d2f 100644 --- a/test-utils/testlib/src/runtime_utils.rs +++ b/test-utils/testlib/src/runtime_utils.rs @@ -4,6 +4,7 @@ use near_primitives::account::{AccessKey, Account}; use near_primitives::hash::hash; use near_primitives::state_record::StateRecord; use near_primitives::types::{AccountId, Balance}; +use near_primitives::version::PROTOCOL_VERSION; pub fn alice_account() -> AccountId { "alice.near".parse().unwrap() @@ -46,7 +47,7 @@ pub fn add_contract(genesis: &mut Genesis, account_id: &AccountId, code: Vec if !is_account_record_found { records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(0, 0, 0, hash, 0), + account: Account::new(0, 0, 0, hash, 0, PROTOCOL_VERSION), }); } records.push(StateRecord::Contract { account_id: account_id.clone(), code }); @@ -63,7 +64,7 @@ pub fn add_account_with_access_key( let records = genesis.force_read_records().as_mut(); records.push(StateRecord::Account { account_id: account_id.clone(), - account: Account::new(balance, 0, 0, Default::default(), 0), + account: Account::new(balance, 0, 0, Default::default(), 0, PROTOCOL_VERSION), }); records.push(StateRecord::AccessKey { account_id, public_key, access_key }); } diff --git a/tools/amend-genesis/src/lib.rs b/tools/amend-genesis/src/lib.rs index 9b392724b9f..a1be5ce8515 100644 --- a/tools/amend-genesis/src/lib.rs +++ b/tools/amend-genesis/src/lib.rs @@ -10,6 +10,7 @@ use near_primitives::utils; use near_primitives::version::ProtocolVersion; use near_primitives_core::account::{AccessKey, Account}; use near_primitives_core::types::{Balance, BlockHeightDelta, NumBlocks, NumSeats, NumShards}; +use near_primitives_core::version::PROTOCOL_VERSION; use num_rational::Rational32; use serde::ser::{SerializeSeq, Serializer}; use std::collections::{hash_map, HashMap}; @@ -73,8 +74,14 @@ impl AccountRecords { num_bytes_account: u64, ) { assert!(self.account.is_none()); - let account = - Account::new(amount, locked, nonrefundable, CryptoHash::default(), num_bytes_account); + let account = Account::new( + amount, + locked, + nonrefundable, + CryptoHash::default(), + num_bytes_account, + PROTOCOL_VERSION, + ); self.account = Some(account); } @@ -473,6 +480,7 @@ mod test { nonrefundable_balance, CryptoHash::default(), *storage_usage, + PROTOCOL_VERSION, ); StateRecord::Account { account_id: account_id.parse().unwrap(), account } } diff --git a/tools/fork-network/src/cli.rs b/tools/fork-network/src/cli.rs index d68f3841255..bf491df89d2 100644 --- a/tools/fork-network/src/cli.rs +++ b/tools/fork-network/src/cli.rs @@ -752,6 +752,7 @@ impl ForkNetworkCommand { 0, CryptoHash::default(), storage_bytes, + PROTOCOL_VERSION, ), )?; storage_mutator.set_access_key( From 11f0b5e2c545e8ab0df3e78f6b1ed0e2d7f3cf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Thu, 1 Feb 2024 17:00:57 +0100 Subject: [PATCH 33/36] PR fixes --- core/primitives-core/src/account.rs | 25 ++++++++--------- core/primitives/src/test_utils.rs | 2 +- .../client/features/nonrefundable_transfer.rs | 2 +- .../tests/client/features/wallet_contract.rs | 27 ++++++++++++++++--- runtime/runtime/src/actions.rs | 2 +- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 03770764c3c..6d1fcce57d2 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -76,6 +76,8 @@ impl Account { /// differentiate AccountVersion V1 from newer versions. const SERIALIZATION_SENTINEL: u128 = u128::MAX; + // TODO(nonrefundable) Consider using consider some additional newtypes + // or a different way to write down constructor (e.g. builder pattern.) pub fn new( amount: Balance, locked: Balance, @@ -180,10 +182,6 @@ impl Account { /// These accounts are serialized in merklized state. /// We keep old accounts in the old format to avoid migration of the MPT. #[derive(BorshSerialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] -#[cfg_attr( - not(feature = "protocol_feature_nonrefundable_transfer_nep491"), - derive(BorshDeserialize) -)] struct LegacyAccount { amount: Balance, locked: Balance, @@ -233,14 +231,14 @@ impl<'de> serde::Deserialize<'de> for Account { let version = match account_data.version { Some(version) => version, None => { - return Err(serde::de::Error::custom("Missing `version` field")); + return Err(serde::de::Error::custom("missing `version` field")); } }; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] if version < AccountVersion::V2 && nonrefundable > 0 { return Err(serde::de::Error::custom( - "Non-refundable positive amount exists for account version older than V2", + "non-refundable positive amount exists for account version older than V2", )); } @@ -271,13 +269,19 @@ impl BorshDeserialize for Account { // either a sentinel or a balance. let sentinel_or_amount = u128::deserialize_reader(rd)?; if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("account serialization sentinel must not occur before NEP-491 landed"), + )); + // Account v2 or newer. let version_byte = u8::deserialize_reader(rd)?; let version = AccountVersion::try_from(version_byte).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, format!( - "Error deserializing account: invalid account version {}", + "error deserializing account: invalid account version {}", version_byte ), ) @@ -286,7 +290,7 @@ impl BorshDeserialize for Account { if version < AccountVersion::V2 { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("Expected account version 2 or higher, got {:?}", version), + format!("expected account version 2 or higher, got {:?}", version), )); } let account = AccountV2::deserialize_reader(rd)?; @@ -341,10 +345,7 @@ impl BorshSerialize for Account { // of unique binary representation. AccountVersion::V1 => { if self.nonrefundable > 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Trying to serialize V1 account with nonrefundable amount"), - )); + panic!("Trying to serialize V1 account with nonrefundable amount"); } legacy_account.serialize(writer) } diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 634c41d165d..68e69ae4934 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -683,7 +683,7 @@ impl FinalExecutionOutcomeView { } /// Calculates how much NEAR was burnt for gas, after refunds. - pub fn gas_cost(&self) -> Balance { + pub fn tokens_burnt(&self) -> Balance { self.transaction_outcome.outcome.tokens_burnt + self.receipts_outcome.iter().map(|r| r.outcome.tokens_burnt).sum::() } diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 2d92fe9ccba..07063eefe46 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -197,7 +197,7 @@ fn exec_transfers( return tx_result; } - let gas_cost = outcome.gas_cost(); + let gas_cost = outcome.tokens_burnt(); assert_eq!( sender_pre_balance - config.transfers.regular_amount diff --git a/integration-tests/src/tests/client/features/wallet_contract.rs b/integration-tests/src/tests/client/features/wallet_contract.rs index 90212a0a719..0e8441a23c6 100644 --- a/integration-tests/src/tests/client/features/wallet_contract.rs +++ b/integration-tests/src/tests/client/features/wallet_contract.rs @@ -13,10 +13,13 @@ use near_primitives::transaction::{ TransferAction, }; use near_primitives::utils::derive_eth_implicit_account_id; -use near_primitives::views::{FinalExecutionStatus, QueryRequest, QueryResponseKind}; +use near_primitives::views::{ + FinalExecutionStatus, QueryRequest, QueryResponse, QueryResponseKind, +}; use near_primitives_core::{ account::AccessKey, checked_feature, types::BlockHeight, version::PROTOCOL_VERSION, }; +use near_store::ShardUId; use near_vm_runner::ContractCode; use near_wallet_contract::{wallet_contract, wallet_contract_magic_bytes}; use nearcore::{config::GenesisExt, test_utils::TestEnvNightshadeSetupExt, NEAR_BASE}; @@ -45,6 +48,24 @@ fn check_tx_processing( next_height } +fn view_request(env: &TestEnv, request: QueryRequest) -> QueryResponse { + let head = env.clients[0].chain.head().unwrap(); + let head_block = env.clients[0].chain.get_block(&head.last_block_hash).unwrap(); + env.clients[0] + .runtime_adapter + .query( + ShardUId::single_shard(), + &head_block.chunks()[0].prev_state_root(), + head.height, + 0, + &head.prev_block_hash, + &head.last_block_hash, + head_block.header().epoch_id(), + &request, + ) + .unwrap() +} + /// Tests that ETH-implicit account is created correctly, with Wallet Contract hash. #[test] fn test_eth_implicit_account_creation() { @@ -80,7 +101,7 @@ fn test_eth_implicit_account_creation() { // Verify the ETH-implicit account has zero balance and appropriate code hash. // Check that the account storage fits within zero balance account limit. let request = QueryRequest::ViewAccount { account_id: eth_implicit_account_id.clone() }; - match env.query_view(request).unwrap().kind { + match view_request(&env, request).kind { QueryResponseKind::ViewAccount(view) => { assert_eq!(view.amount, 0); assert_eq!(view.code_hash, *magic_bytes.hash()); @@ -91,7 +112,7 @@ fn test_eth_implicit_account_creation() { // Verify that contract code deployed to the ETH-implicit account is near[wallet contract hash]. let request = QueryRequest::ViewCode { account_id: eth_implicit_account_id }; - match env.query_view(request).unwrap().kind { + match view_request(&env, request).kind { QueryResponseKind::ViewCode(view) => { let contract_code = ContractCode::new(view.code, None); assert_eq!(contract_code.hash(), magic_bytes.hash()); diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 71e45594bab..bd24f9adf32 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -412,7 +412,7 @@ pub(crate) fn action_nonrefundable_storage_transfer( ) -> Result<(), StorageError> { account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else(|| { StorageError::StorageInconsistentState( - "Non-refundable account balance integer overflow".to_string(), + "non-refundable account balance integer overflow".to_string(), ) })?); Ok(()) From 0b1a09776a594242f0450c92691e0ddc08ff5867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Fri, 2 Feb 2024 12:21:52 +0100 Subject: [PATCH 34/36] Return error instead of panicking --- core/primitives-core/src/account.rs | 5 ++++- runtime/runtime/src/actions.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 929d0ec3c0a..d030ce13f9c 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -270,7 +270,10 @@ impl BorshDeserialize for Account { let sentinel_or_amount = u128::deserialize_reader(rd)?; if sentinel_or_amount == Account::SERIALIZATION_SENTINEL { if cfg!(not(feature = "protocol_feature_nonrefundable_transfer_nep491")) { - panic!("account serialization sentinel must not occur before NEP-491 landed"); + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("account serialization sentinel not allowed for AccountV1"), + )); } // Account v2 or newer. diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index bd24f9adf32..046ab923c38 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -1011,6 +1011,7 @@ pub(crate) fn check_account_existence( ); } } + // TODO(nonrefundable) Merge with arm above on stabilization. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] Action::NonrefundableStorageTransfer(_) => { if account.is_none() { From 38a7937b8f59a82b6be81120ec913b036a1aa33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Thu, 15 Feb 2024 11:02:25 +0100 Subject: [PATCH 35/36] build fixes --- chain/rosetta-rpc/src/adapters/mod.rs | 15 +++++++++++++++ chain/rosetta-rpc/src/adapters/transactions.rs | 6 +++--- core/primitives/src/action/mod.rs | 7 +++++++ .../src/tests/client/features/in_memory_tries.rs | 9 ++++++++- .../client/features/nonrefundable_transfer.rs | 6 +----- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 3de05c6abec..f1631e86397 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -1034,6 +1034,12 @@ mod tests { deposit: near_primitives::types::Balance::MAX, } .into()]; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let nonrefundable_transfer_actions = + vec![near_primitives::transaction::NonrefundableStorageTransferAction { + deposit: near_primitives::types::Balance::MAX, + } + .into()]; let stake_actions = vec![near_primitives::transaction::StakeAction { stake: 456, public_key: near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519) @@ -1064,6 +1070,13 @@ mod tests { let wallet_style_create_account_actions = [create_account_actions.to_vec(), add_key_actions.to_vec(), transfer_actions.to_vec()] .concat(); + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let wallet_style_create_account_with_nonrefundable_actions = [ + create_account_actions.to_vec(), + add_key_actions.to_vec(), + nonrefundable_transfer_actions.to_vec(), + ] + .concat(); let create_account_and_stake_immediately_actions = [create_account_actions.to_vec(), transfer_actions.to_vec(), stake_actions.to_vec()] .concat(); @@ -1089,6 +1102,8 @@ mod tests { function_call_without_balance_actions, function_call_with_balance_actions, wallet_style_create_account_actions, + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + wallet_style_create_account_with_nonrefundable_actions, create_account_and_stake_immediately_actions, deploy_contract_and_call_it_actions, two_factor_auth_actions, diff --git a/chain/rosetta-rpc/src/adapters/transactions.rs b/chain/rosetta-rpc/src/adapters/transactions.rs index 14fba50e52f..efd2bccacd9 100644 --- a/chain/rosetta-rpc/src/adapters/transactions.rs +++ b/chain/rosetta-rpc/src/adapters/transactions.rs @@ -309,9 +309,9 @@ pub(crate) async fn convert_block_changes_to_transactions( .actions .iter() .map(|action| match action { - near_primitives::views::ActionView::Transfer { - deposit, .. - } => *deposit, + near_primitives::views::ActionView::Transfer { deposit } => { + *deposit + } _ => 0, }) .sum::(); diff --git a/core/primitives/src/action/mod.rs b/core/primitives/src/action/mod.rs index 91f6f3f1465..aa266a0f321 100644 --- a/core/primitives/src/action/mod.rs +++ b/core/primitives/src/action/mod.rs @@ -250,6 +250,13 @@ impl From for Action { } } +#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] +impl From for Action { + fn from(nonrefundable_transfer_action: NonrefundableStorageTransferAction) -> Self { + Self::NonrefundableStorageTransfer(nonrefundable_transfer_action) + } +} + impl From for Action { fn from(stake_action: StakeAction) -> Self { Self::Stake(Box::new(stake_action)) diff --git a/integration-tests/src/tests/client/features/in_memory_tries.rs b/integration-tests/src/tests/client/features/in_memory_tries.rs index 1f36e59a62b..0ede291801c 100644 --- a/integration-tests/src/tests/client/features/in_memory_tries.rs +++ b/integration-tests/src/tests/client/features/in_memory_tries.rs @@ -547,7 +547,14 @@ fn test_in_memory_trie_consistency_with_state_sync_base_case(track_all_shards: b let staked = if i < NUM_VALIDATORS { validator_stake } else { 0 }; records.push(StateRecord::Account { account_id: account.clone(), - account: Account::new(initial_balance, staked, CryptoHash::default(), 0), + account: Account::new( + initial_balance, + staked, + 0, + CryptoHash::default(), + 0, + genesis_config.protocol_version, + ), }); records.push(StateRecord::AccessKey { account_id: account.clone(), diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 07063eefe46..66ceb6c5469 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -6,7 +6,6 @@ //! //! NEP: https://github.com/near/NEPs/pull/491 -use near_chain::ChainGenesis; use near_chain_configs::Genesis; use near_client::test_utils::TestEnv; use near_crypto::{InMemorySigner, KeyType, PublicKey}; @@ -80,10 +79,7 @@ fn setup_env_with_protocol_version(protocol_version: Option) -> if let Some(protocol_version) = protocol_version { genesis.config.protocol_version = protocol_version; } - TestEnv::builder(ChainGenesis::new(&genesis)) - .real_epoch_managers(&genesis.config) - .nightshade_runtimes(&genesis) - .build() + TestEnv::builder(&genesis.config).nightshade_runtimes(&genesis).build() } /// Creates a test environment using default protocol version. From f49adef01e2ed2d81138db5773b7587f9ce79848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Chuda=C5=9B?= Date: Thu, 15 Feb 2024 13:00:12 +0100 Subject: [PATCH 36/36] Add tests --- chain/rosetta-rpc/src/adapters/mod.rs | 44 ++++++++++++++-------- core/chain-configs/src/genesis_validate.rs | 19 ++++++++++ core/primitives-core/src/account.rs | 29 ++++++++++++++ 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index f1631e86397..e4eca108050 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -867,6 +867,35 @@ mod tests { use near_primitives::action::delegate::{DelegateAction, SignedDelegateAction}; use near_primitives::transaction::{Action, TransferAction}; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + #[test] + fn test_convert_nonrefundable_storage_transfer_action() { + let transfer_actions = vec![near_primitives::transaction::TransferAction { + deposit: near_primitives::types::Balance::MAX, + } + .into()]; + let nonrefundable_transfer_actions = + vec![near_primitives::transaction::NonrefundableStorageTransferAction { + deposit: near_primitives::types::Balance::MAX, + } + .into()]; + let near_transfer_actions = NearActions { + sender_account_id: "sender.near".parse().unwrap(), + receiver_account_id: "receiver.near".parse().unwrap(), + actions: transfer_actions, + }; + let near_nonrefundable_transfer_actions = NearActions { + sender_account_id: "sender.near".parse().unwrap(), + receiver_account_id: "receiver.near".parse().unwrap(), + actions: nonrefundable_transfer_actions, + }; + let transfer_operations_converted: Vec = + near_transfer_actions.into(); + let nonrefundable_transfer_operations_converted: Vec = + near_nonrefundable_transfer_actions.into(); + assert_eq!(transfer_operations_converted, nonrefundable_transfer_operations_converted); + } + #[test] fn test_convert_block_changes_to_transactions() { run_actix(async { @@ -1034,12 +1063,6 @@ mod tests { deposit: near_primitives::types::Balance::MAX, } .into()]; - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let nonrefundable_transfer_actions = - vec![near_primitives::transaction::NonrefundableStorageTransferAction { - deposit: near_primitives::types::Balance::MAX, - } - .into()]; let stake_actions = vec![near_primitives::transaction::StakeAction { stake: 456, public_key: near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519) @@ -1070,13 +1093,6 @@ mod tests { let wallet_style_create_account_actions = [create_account_actions.to_vec(), add_key_actions.to_vec(), transfer_actions.to_vec()] .concat(); - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let wallet_style_create_account_with_nonrefundable_actions = [ - create_account_actions.to_vec(), - add_key_actions.to_vec(), - nonrefundable_transfer_actions.to_vec(), - ] - .concat(); let create_account_and_stake_immediately_actions = [create_account_actions.to_vec(), transfer_actions.to_vec(), stake_actions.to_vec()] .concat(); @@ -1102,8 +1118,6 @@ mod tests { function_call_without_balance_actions, function_call_with_balance_actions, wallet_style_create_account_actions, - #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - wallet_style_create_account_with_nonrefundable_actions, create_account_and_stake_immediately_actions, deploy_contract_and_call_it_actions, two_factor_auth_actions, diff --git a/core/chain-configs/src/genesis_validate.rs b/core/chain-configs/src/genesis_validate.rs index 8888771cced..8e917de61f6 100644 --- a/core/chain-configs/src/genesis_validate.rs +++ b/core/chain-configs/src/genesis_validate.rs @@ -208,6 +208,25 @@ mod test { Account::new(100, 10, 0, Default::default(), 0, PROTOCOL_VERSION) } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + #[test] + fn test_total_supply_includes_nonrefundable_amount() { + let mut config = GenesisConfig::default(); + config.epoch_length = 42; + config.total_supply = 111; + config.validators = vec![AccountInfo { + account_id: "test".parse().unwrap(), + public_key: VALID_ED25519_RISTRETTO_KEY.parse().unwrap(), + amount: 10, + }]; + let records = GenesisRecords(vec![StateRecord::Account { + account_id: "test".parse().unwrap(), + account: Account::new(100, 10, 1, Default::default(), 0, PROTOCOL_VERSION), + }]); + let genesis = &Genesis::new(config, records).unwrap(); + validate_genesis(genesis).unwrap(); + } + #[test] #[should_panic(expected = "wrong total supply")] fn test_total_supply_not_match() { diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index d030ce13f9c..96b44379b09 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -466,9 +466,23 @@ pub struct FunctionCallPermission { mod tests { use crate::hash::hash; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + use crate::version::ProtocolFeature; use super::*; + #[test] + #[should_panic] + fn test_v1_account_cannot_have_nonrefundable_amount() { + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let protocol_version = crate::version::PROTOCOL_VERSION; + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let protocol_version = ProtocolFeature::NonRefundableBalance.protocol_version() - 1; + + Account::new(0, 0, 1, CryptoHash::default(), 0, protocol_version); + } + #[test] fn test_legacy_account_serde_serialization() { let old_account = LegacyAccount { @@ -574,6 +588,21 @@ mod tests { assert_eq!(deserialized_account, account); } + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + #[test] + #[should_panic(expected = "account serialization sentinel not allowed for AccountV1")] + fn test_account_v1_borsh_serialization_sentinel() { + let account = Account { + amount: Account::SERIALIZATION_SENTINEL, + locked: 1_000_000, + code_hash: CryptoHash::default(), + storage_usage: 100, + version: AccountVersion::V1, + }; + let serialized_account = borsh::to_vec(&account).unwrap(); + ::deserialize(&mut &serialized_account[..]).unwrap(); + } + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] /// It is impossible to construct V1 account with nonrefundable amount greater than 0. /// So the situation in this test is theoretical.