diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index be2edcc8999..e2f1a5d2f82 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -33,7 +33,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_snapshot!(hash, @"Dnn7UkUiRfMu13jheAbcoJvQ66gKBgAoynRdEkMTkX58"); + insta::assert_snapshot!(hash, @"CyVdmcpdfz8VAqZFN4zbZLTRcbcnAUzRJwNgxbgeEUMU"); } else { insta::assert_snapshot!(hash, @"EsUNazp4zR2XgiwZSuQnX9dsaFk1VDhdRwGYt1YHpu5b"); } @@ -51,7 +51,7 @@ fn build_chain() { let hash = chain.head().unwrap().last_block_hash; if cfg!(feature = "nightly") { - insta::assert_snapshot!(hash, @"8xU6fbbdGeYmQRaAbsszMHDM88KSAaniHykowTUDYXpm"); + insta::assert_snapshot!(hash, @"72j1xRcBZpPtyo2rpPBPRspL6Q9LCju2Doa8KFhYPNJt"); } else { insta::assert_snapshot!(hash, @"CJ5p62dVTMVgRADQrWPkFLrozDN8KxbKMGqjVkPXBD7W"); } diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json index 85a209dc090..5725f0c95e1 100644 --- a/chain/jsonrpc/res/rpc_errors_schema.json +++ b/chain/jsonrpc/res/rpc_errors_schema.json @@ -47,7 +47,7 @@ "DelegateActionAccessKeyError", "DelegateActionInvalidNonce", "DelegateActionNonceTooLarge", - "NonRefundableBalanceToExistingAccount" + "NonRefundableTransferToExistingAccount" ], "props": { "index": "" @@ -641,8 +641,8 @@ "subtypes": [], "props": {} }, - "NonRefundableBalanceToExistingAccount": { - "name": "NonRefundableBalanceToExistingAccount", + "NonRefundableTransferToExistingAccount": { + "name": "NonRefundableTransferToExistingAccount", "subtypes": [], "props": { "account_id": "" diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index fc2eead89d3..ac92f44f9c8 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -353,8 +353,7 @@ impl From for Vec { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - // Both refundable and non-refundable transfers are considered as available balance. - // TODO(nonrefundable) Merge with the arm above on stabilization. + // Non-refundable transfer deposit is burnt for permanent storage bytes on the receiving account. near_primitives::transaction::Action::NonrefundableStorageTransfer(action) => { let transfer_amount = crate::models::Amount::from_yoctonear(action.deposit); @@ -368,18 +367,6 @@ impl From for Vec { } .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], - ), - ); } near_primitives::transaction::Action::Stake(action) => { @@ -868,35 +855,6 @@ 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 { @@ -922,7 +880,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -939,7 +897,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -954,7 +912,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -971,7 +929,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -986,7 +944,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -998,7 +956,7 @@ mod tests { code_hash: near_primitives::hash::CryptoHash::default(), locked: 400000000000000000000000000000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, storage_paid_at: 0, storage_usage: 200000, }, @@ -1629,4 +1587,33 @@ mod tests { Err(crate::errors::ErrorKind::InvalidInput(_)) )); } + + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + #[test] + fn test_convert_nonrefundable_storage_transfer_action() { + let deposit = near_primitives::types::Balance::MAX - 1; + let nonrefundable_transfer_actions = vec![ + near_primitives::transaction::NonrefundableStorageTransferAction { deposit }.into(), + ]; + 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 nonrefundable_transfer_operations_converted: Vec = + near_nonrefundable_transfer_actions.into(); + assert_eq!(nonrefundable_transfer_operations_converted.len(), 1); + assert_eq!( + nonrefundable_transfer_operations_converted[0].type_, + crate::models::OperationType::Transfer + ); + assert_eq!( + nonrefundable_transfer_operations_converted[0].account, + "sender.near".parse().unwrap() + ); + assert_eq!( + nonrefundable_transfer_operations_converted[0].amount, + Some(-crate::models::Amount::from_yoctonear(deposit)) + ); + } } diff --git a/core/chain-configs/Cargo.toml b/core/chain-configs/Cargo.toml index 38fd5234e44..30495c6411f 100644 --- a/core/chain-configs/Cargo.toml +++ b/core/chain-configs/Cargo.toml @@ -33,6 +33,7 @@ near-primitives.workspace = true near-config-utils.workspace = true [features] +protocol_feature_nonrefundable_transfer_nep491 = [] nightly_protocol = [ "near-async/nightly_protocol", "near-o11y/nightly_protocol", @@ -45,6 +46,7 @@ nightly = [ "near-parameters/nightly", "near-primitives/nightly", "nightly_protocol", + "protocol_feature_nonrefundable_transfer_nep491", ] default = [] metrics = ["near-o11y"] diff --git a/core/chain-configs/src/genesis_validate.rs b/core/chain-configs/src/genesis_validate.rs index 8e917de61f6..b2cb11803d6 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() + account.nonrefundable(); + self.total_supply += account.locked() + account.amount(); self.account_ids.insert(account_id.clone()); if account.locked() > 0 { self.staked_accounts.insert(account_id.clone(), account.locked()); @@ -210,10 +210,10 @@ mod test { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] #[test] - fn test_total_supply_includes_nonrefundable_amount() { + fn test_total_supply_does_not_depend_on_permanent_storage_bytes() { let mut config = GenesisConfig::default(); config.epoch_length = 42; - config.total_supply = 111; + config.total_supply = 110; config.validators = vec![AccountInfo { account_id: "test".parse().unwrap(), public_key: VALID_ED25519_RISTRETTO_KEY.parse().unwrap(), diff --git a/core/primitives-core/src/account.rs b/core/primitives-core/src/account.rs index 96b44379b09..031d7070fbc 100644 --- a/core/primitives-core/src/account.rs +++ b/core/primitives-core/src/account.rs @@ -48,16 +48,16 @@ impl TryFrom for AccountVersion { )] #[derive(serde::Serialize, 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. #[serde(with = "dec_format")] locked: Balance, - /// Tokens that are not available to withdraw, stake, or refund, but can be used to cover storage usage. + /// Permanent storage allowance, additional to what storage staking gives. #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] #[serde(with = "dec_format")] - nonrefundable: Balance, + permanent_storage_bytes: StorageUsage, /// 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. @@ -81,7 +81,7 @@ impl Account { pub fn new( amount: Balance, locked: Balance, - nonrefundable: Balance, + permanent_storage_bytes: StorageUsage, code_hash: CryptoHash, storage_usage: StorageUsage, #[cfg_attr(not(feature = "protocol_feature_nonrefundable_transfer_nep491"), allow(unused))] @@ -91,20 +91,20 @@ impl Account { let account_version = AccountVersion::V1; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let account_version = if checked_feature!("stable", NonRefundableBalance, protocol_version) + let account_version = if checked_feature!("stable", NonrefundableStorage, protocol_version) { AccountVersion::V2 } else { AccountVersion::V1 }; if account_version == AccountVersion::V1 { - assert_eq!(nonrefundable, 0); + assert_eq!(permanent_storage_bytes, 0); } Account { amount, locked, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable, + permanent_storage_bytes, code_hash, storage_usage, version: account_version, @@ -118,13 +118,13 @@ impl Account { #[inline] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn nonrefundable(&self) -> Balance { - self.nonrefundable + pub fn permanent_storage_bytes(&self) -> StorageUsage { + self.permanent_storage_bytes } #[inline] #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - pub fn nonrefundable(&self) -> Balance { + pub fn permanent_storage_bytes(&self) -> StorageUsage { 0 } @@ -155,8 +155,8 @@ impl Account { #[inline] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub fn set_nonrefundable(&mut self, nonrefundable: Balance) { - self.nonrefundable = nonrefundable; + pub fn set_permanent_storage_bytes(&mut self, permanent_storage_bytes: StorageUsage) { + self.permanent_storage_bytes = permanent_storage_bytes; } #[inline] @@ -197,7 +197,7 @@ struct AccountV2 { code_hash: CryptoHash, storage_usage: StorageUsage, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: Balance, + permanent_storage_bytes: StorageUsage, } /// We need custom serde deserialization in order to parse mainnet genesis accounts (LegacyAccounts) @@ -216,7 +216,7 @@ impl<'de> serde::Deserialize<'de> for Account { locked: Balance, // If the field is missing, serde will use None as the default. #[serde(default, with = "dec_format")] - nonrefundable: Option, + permanent_storage_bytes: Option, code_hash: CryptoHash, storage_usage: StorageUsage, #[serde(default)] @@ -225,9 +225,9 @@ impl<'de> serde::Deserialize<'de> for Account { let account_data = AccountData::deserialize(deserializer)?; - match account_data.nonrefundable { - Some(nonrefundable) => { - // Given that the `nonrefundable` field has been serialized, the `version` field must has been serialized too. + match account_data.permanent_storage_bytes { + Some(permanent_storage_bytes) => { + // Given that the `permanent_storage_bytes` field has been serialized, the `version` field must has been serialized too. let version = match account_data.version { Some(version) => version, None => { @@ -236,9 +236,9 @@ impl<'de> serde::Deserialize<'de> for Account { }; #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - if version < AccountVersion::V2 && nonrefundable > 0 { + if version < AccountVersion::V2 && permanent_storage_bytes > 0 { return Err(serde::de::Error::custom( - "non-refundable positive amount exists for account version older than V2", + "permanent storage bytes positive amount exists for account version older than V2", )); } @@ -247,7 +247,7 @@ impl<'de> serde::Deserialize<'de> for Account { locked: account_data.locked, code_hash: account_data.code_hash, storage_usage: account_data.storage_usage, - nonrefundable, + permanent_storage_bytes, version, }) } @@ -256,7 +256,7 @@ impl<'de> serde::Deserialize<'de> for Account { locked: account_data.locked, code_hash: account_data.code_hash, storage_usage: account_data.storage_usage, - nonrefundable: 0, + permanent_storage_bytes: 0, version: AccountVersion::V1, }), } @@ -300,7 +300,7 @@ impl BorshDeserialize for Account { amount: account.amount, locked: account.locked, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: account.nonrefundable, + permanent_storage_bytes: account.permanent_storage_bytes, code_hash: account.code_hash, storage_usage: account.storage_usage, version, @@ -318,7 +318,7 @@ impl BorshDeserialize for Account { storage_usage, version: AccountVersion::V1, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, }) } } @@ -345,8 +345,8 @@ impl BorshSerialize for Account { // while serializing. But that would break the borsh assumptions // of unique binary representation. AccountVersion::V1 => { - if self.nonrefundable > 0 { - panic!("Trying to serialize V1 account with nonrefundable amount"); + if self.permanent_storage_bytes > 0 { + panic!("Trying to serialize V1 account with permanent_storage_bytes"); } legacy_account.serialize(writer) } @@ -356,7 +356,7 @@ impl BorshSerialize for Account { locked: self.locked(), code_hash: self.code_hash(), storage_usage: self.storage_usage(), - nonrefundable: self.nonrefundable(), + permanent_storage_bytes: self.permanent_storage_bytes(), }; let sentinel = Account::SERIALIZATION_SENTINEL; // For now a constant, but if we need V3 later we can use this @@ -473,12 +473,12 @@ mod tests { #[test] #[should_panic] - fn test_v1_account_cannot_have_nonrefundable_amount() { + fn test_v1_account_cannot_have_permanent_storage_bytes() { #[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; + let protocol_version = ProtocolFeature::NonrefundableStorage.protocol_version() - 1; Account::new(0, 0, 1, CryptoHash::default(), 0, protocol_version); } @@ -498,7 +498,7 @@ mod tests { 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.permanent_storage_bytes(), 0); assert_eq!(new_account.version, AccountVersion::V1); let new_serialized_account = serde_json::to_string(&new_account).unwrap(); @@ -535,7 +535,7 @@ mod tests { amount: 10_000_000, locked: 100_000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, code_hash: CryptoHash::default(), storage_usage: 1000, version: AccountVersion::V1, @@ -546,17 +546,17 @@ mod tests { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - /// It is impossible to construct V1 account with nonrefundable amount greater than 0. + /// It is impossible to construct V1 account with permanent_storage_bytes 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, + /// Serialization of account V1 with permanent_storage_bytes 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, + permanent_storage_bytes: 1, code_hash: CryptoHash::default(), storage_usage: 1000, version: AccountVersion::V1, @@ -573,7 +573,7 @@ mod tests { amount: 1_000_000, locked: 1_000_000, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: 0, + permanent_storage_bytes: 0, code_hash: CryptoHash::default(), storage_usage: 100, version: AccountVersion::V1, @@ -604,17 +604,17 @@ mod tests { } #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - /// It is impossible to construct V1 account with nonrefundable amount greater than 0. + /// It is impossible to construct V1 account with permanent_storage_bytes greater than 0. /// So the situation in this test is theoretical. /// - /// If a V1 account had nonrefundable amount greater than zero, it would panic during Borsh serialization. + /// If a V1 account had permanent_storage_bytes greater than zero, it would panic during Borsh serialization. #[test] - #[should_panic(expected = "Trying to serialize V1 account with nonrefundable amount")] + #[should_panic(expected = "Trying to serialize V1 account with permanent_storage_bytes")] fn test_account_v1_borsh_serialization_invalid_data() { let account = Account { amount: 1_000_000, locked: 1_000_000, - nonrefundable: 1, + permanent_storage_bytes: 1, code_hash: CryptoHash::default(), storage_usage: 100, version: AccountVersion::V1, @@ -628,7 +628,7 @@ mod tests { let account = Account { amount: 10_000_000, locked: 100_000, - nonrefundable: 37, + permanent_storage_bytes: 37, code_hash: CryptoHash::default(), storage_usage: 1000, version: AccountVersion::V2, @@ -644,14 +644,14 @@ mod tests { let account = Account { amount: 1_000_000, locked: 1_000_000, - nonrefundable: 42, + permanent_storage_bytes: 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!("A3Ypkhkm6G5PYwHZw1eKYVunEzafLu8fbTAYLGts2AGy") + expect_test::expect!("G2Dn8ABMPqsoXuQCPR9yV19HgBi3ZJNqEMEntyLqveDR") } else { expect_test::expect!("EVk5UaxBe8LQ8r8iD5EAxVBs6TJcMDKqyH7PBuho6bBJ") } diff --git a/core/primitives-core/src/version.rs b/core/primitives-core/src/version.rs index a1f2ac45134..ce12dcbe518 100644 --- a/core/primitives-core/src/version.rs +++ b/core/primitives-core/src/version.rs @@ -128,7 +128,7 @@ pub enum ProtocolFeature { /// 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_transfer_nep491")] - NonRefundableBalance, + NonrefundableStorage, RestrictTla, /// Increases the number of chunk producers. TestnetFewerBlockProducers, @@ -212,7 +212,7 @@ impl ProtocolFeature { ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => 132, ProtocolFeature::EthImplicitAccounts => 138, #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - ProtocolFeature::NonRefundableBalance => 140, + ProtocolFeature::NonrefundableStorage => 140, #[cfg(feature = "statelessnet_protocol")] ProtocolFeature::SimpleNightshadeV3 => 141, } diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index aab6214220d..5e299c9ef91 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -508,8 +508,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 }, + /// Non-refundable storage transfer to an existing account is not allowed according to NEP-491. + NonRefundableTransferToExistingAccount { account_id: AccountId }, } impl From for ActionError { @@ -834,8 +834,8 @@ 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) + ActionErrorKind::NonRefundableTransferToExistingAccount { account_id} => { + write!(f, "Can't make non-refundable storage transfer to {} because it already exists", account_id) } } } diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 75cd1afa88f..900f2445bdb 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -62,7 +62,7 @@ pub struct AccountView { pub locked: Balance, #[serde(with = "dec_format")] #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - pub nonrefundable: Balance, + pub permanent_storage_bytes: StorageUsage, pub code_hash: CryptoHash, pub storage_usage: StorageUsage, /// TODO(2271): deprecated. @@ -107,7 +107,7 @@ impl From<&Account> for AccountView { amount: account.amount(), locked: account.locked(), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - nonrefundable: account.nonrefundable(), + permanent_storage_bytes: account.permanent_storage_bytes(), code_hash: account.code_hash(), storage_usage: account.storage_usage(), storage_paid_at: 0, @@ -124,13 +124,13 @@ impl From for AccountView { impl From<&AccountView> for Account { fn from(view: &AccountView) -> Self { #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - let nonrefundable = view.nonrefundable; + let permanent_storage_bytes = view.permanent_storage_bytes; #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] - let nonrefundable = 0; + let permanent_storage_bytes = 0; Account::new( view.amount, view.locked, - nonrefundable, + permanent_storage_bytes, view.code_hash, view.storage_usage, PROTOCOL_VERSION, diff --git a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs index 786484d667f..c1d63d0d01e 100644 --- a/integration-tests/src/tests/client/features/nonrefundable_transfer.rs +++ b/integration-tests/src/tests/client/features/nonrefundable_transfer.rs @@ -17,6 +17,7 @@ use near_primitives::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeployContractAction, NonrefundableStorageTransferAction, SignedTransaction, TransferAction, }; +use near_primitives::types::StorageUsage; 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}; @@ -46,6 +47,9 @@ const TEST_CASES: [Transfers; 3] = [ Transfers { regular_amount: 1, nonrefundable_amount: 1, nonrefundable_transfer_first: false }, ]; +/// Contract size that does not fit within Zero-balance account limit. +const TEST_CONTRACT_SIZE: usize = 1500; + struct TransferConfig { /// Describes transfers configuration we are interested in. transfers: Transfers, @@ -67,6 +71,10 @@ fn receiver() -> AccountId { "test1".parse().unwrap() } +fn new_account_id(index: usize) -> AccountId { + format!("subaccount{}.test0", index).parse().unwrap() +} + /// Default signer (corresponding to the default sender) to use in tests of this module. fn signer() -> InMemorySigner { InMemorySigner::from_seed(sender(), KeyType::ED25519, "test0") @@ -107,6 +115,12 @@ fn account_exists(env: &mut TestEnv, account_id: AccountId) -> bool { env.query_view(request).is_ok() } +fn get_total_supply(env: &TestEnv) -> Balance { + let tip = env.clients[0].chain.head().unwrap(); + let block_info = env.clients[0].chain.get_block_header(&tip.last_block_hash).unwrap(); + block_info.total_supply() +} + fn execute_transaction_from_actions( env: &mut TestEnv, actions: Vec, @@ -143,12 +157,13 @@ fn exec_transfers( config: TransferConfig, ) -> Result { let sender_pre_balance = env.query_balance(sender()); - let (receiver_before_amount, receiver_before_nonrefundable) = if config.account_creation { - (0, 0) - } else { - let receiver_before = env.query_account(receiver.clone()); - (receiver_before.amount, receiver_before.nonrefundable) - }; + let (receiver_before_amount, receiver_before_permanent_storage_bytes) = + if config.account_creation { + (0, 0) + } else { + let receiver_before = env.query_account(receiver.clone()); + (receiver_before.amount, receiver_before.permanent_storage_bytes) + }; let mut actions = vec![]; @@ -175,7 +190,7 @@ fn exec_transfers( } if config.deploy_contract { - let contract = near_test_contracts::sized_contract(1500 as usize); + let contract = near_test_contracts::sized_contract(TEST_CONTRACT_SIZE); actions.push(Action::DeployContract(DeployContractAction { code: contract.to_vec() })) } @@ -202,15 +217,42 @@ fn exec_transfers( ); 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_expected_permanent_storage_bytes_after = receiver_before_permanent_storage_bytes + + (config.transfers.nonrefundable_amount / fee_helper().rt_cfg.storage_amount_per_byte()) + as StorageUsage; let receiver_after = env.query_account(receiver); assert_eq!(receiver_after.amount, receiver_expected_amount_after); - assert_eq!(receiver_after.nonrefundable, receiver_expected_non_refundable_after); + assert_eq!( + receiver_after.permanent_storage_bytes, + receiver_expected_permanent_storage_bytes_after + ); tx_result } +fn regular_transfer( + env: &mut TestEnv, + signer: InMemorySigner, + receiver: AccountId, + amount: Balance, +) -> Result { + exec_transfers( + env, + signer, + receiver, + TransferConfig { + transfers: Transfers { + regular_amount: amount, + nonrefundable_amount: 0, + nonrefundable_transfer_first: false, + }, + account_creation: false, + implicit_account_creation: false, + deploy_contract: false, + }, + ) +} + fn delete_account( env: &mut TestEnv, signer: &InMemorySigner, @@ -220,11 +262,11 @@ fn delete_account( execute_transaction_from_actions(env, actions, &signer, signer.account_id.clone()) } -/// Can delete account with non-refundable storage. +/// Can delete account with permanent storage bytes. #[test] -fn deleting_account_with_non_refundable_storage() { +fn deleting_account_with_permanent_storage_bytes() { let mut env = setup_env(); - let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let new_account_id = new_account_id(0); let new_account = InMemorySigner::from_seed( new_account_id.clone(), KeyType::ED25519, @@ -232,7 +274,7 @@ fn deleting_account_with_non_refundable_storage() { ); let regular_amount = 10u128.pow(20); let nonrefundable_amount = NEAR_BASE; - // Create account with non-refundable storage. + // Create account with permanent storage bytes. // 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_transfers( @@ -252,34 +294,37 @@ fn deleting_account_with_non_refundable_storage() { ); create_account_tx_result.unwrap().assert_success(); - // Delete the new account (that has 1 NEAR of non-refundable balance). + // Delete the new account (that has permanent storage bytes worth 1 NEAR). 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. + // Check that the beneficiary account received the remaining balance from the deleted account but nothing more. + // Especially, check that the permanent storage bytes of the beneficiary account were not affected. let beneficiary_after = env.query_account(beneficiary_id); assert_eq!( beneficiary_after.amount, beneficiary_before.amount + regular_amount - fee_helper().prepaid_delete_account_cost() ); - assert_eq!(beneficiary_after.nonrefundable, beneficiary_before.nonrefundable); + assert_eq!( + beneficiary_after.permanent_storage_bytes, + beneficiary_before.permanent_storage_bytes + ); } -/// Non-refundable balance cannot be transferred. +/// Permanent storage bytes cannot be transferred. #[test] -fn non_refundable_balance_cannot_be_transferred() { +fn permanent_storage_bytes_cannot_be_transferred() { let mut env = setup_env(); - let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let new_account_id = new_account_id(0); let new_account = InMemorySigner::from_seed( new_account_id.clone(), KeyType::ED25519, new_account_id.as_str(), ); - // The `new_account` is created with 1 NEAR non-refundable balance. + // The `new_account` is created with permanent storage bytes worth 1 NEAR. let create_account_tx_result = exec_transfers( &mut env, signer(), @@ -297,7 +342,7 @@ fn non_refundable_balance_cannot_be_transferred() { ); create_account_tx_result.unwrap().assert_success(); - // Although `new_account` has 1 NEAR non-refundable balance, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. + // Although `new_account` has permanent storage bytes worth 1 NEAR, it cannot make neither refundable nor non-refundable transfer of 1 yoctoNEAR. for nonrefundable in [false, true] { let transfer_tx_result = exec_transfers( &mut env, @@ -324,11 +369,25 @@ fn non_refundable_balance_cannot_be_transferred() { } } -/// Non-refundable balance allows to have account with zero balance and more than 1kB of state. +fn calculate_named_account_storage_usage(contract_size: usize) -> u64 { + let account_id = new_account_id(0); + let mut account_storage_usage = fee_helper().cfg().storage_usage_config.num_bytes_account; + account_storage_usage += fee_helper().cfg().storage_usage_config.num_extra_bytes_record; + account_storage_usage += + PublicKey::from_seed(KeyType::ED25519, account_id.as_str()).len() as u64; + account_storage_usage += borsh::object_length(&AccessKey::full_access()).unwrap() as u64; + account_storage_usage + contract_size as u64 +} + +/// Permanent storage bytes allows to have account with zero balance and more than 1kB of state. #[test] -fn non_refundable_balance_allows_1kb_state_with_zero_balance() { +fn permanent_storage_bytes_allows_1kb_state_with_zero_balance() { + assert!(fee_helper().rt_cfg.storage_amount_per_byte() > 0); let mut env = setup_env(); - let new_account_id: AccountId = "subaccount.test0".parse().unwrap(); + let new_account_id = new_account_id(0); + let storage_bytes_required = calculate_named_account_storage_usage(TEST_CONTRACT_SIZE) as u128; + let nonrefundable_amount_required = + storage_bytes_required * fee_helper().rt_cfg.storage_amount_per_byte(); let tx_result = exec_transfers( &mut env, signer(), @@ -336,7 +395,34 @@ fn non_refundable_balance_allows_1kb_state_with_zero_balance() { TransferConfig { transfers: Transfers { regular_amount: 0, - nonrefundable_amount: NEAR_BASE / 5, + nonrefundable_amount: nonrefundable_amount_required, + nonrefundable_transfer_first: true, + }, + account_creation: true, + implicit_account_creation: false, + deploy_contract: true, + }, + ); + tx_result.unwrap().assert_success(); +} + +/// 1 yoctonear less than required to pay for necessary permanent storage bytes. +#[test] +fn insufficient_nonrefundable_transfer_amount() { + let mut env = setup_env(); + let new_account_id = new_account_id(0); + let storage_bytes_required = calculate_named_account_storage_usage(TEST_CONTRACT_SIZE) as u128; + let nonrefundable_amount_required = + storage_bytes_required * fee_helper().rt_cfg.storage_amount_per_byte(); + let tx_result = exec_transfers( + &mut env, + signer(), + new_account_id.clone(), + TransferConfig { + transfers: Transfers { + regular_amount: 0, + // We subtract 1 yoctonear here. + nonrefundable_amount: nonrefundable_amount_required - 1, nonrefundable_transfer_first: true, }, account_creation: true, @@ -344,15 +430,121 @@ fn non_refundable_balance_allows_1kb_state_with_zero_balance() { deploy_contract: true, }, ); + let status = &tx_result.unwrap().receipts_outcome[0].outcome.status; + assert!(matches!( + status, + ExecutionStatusView::Failure(TxExecutionError::ActionError( + ActionError { kind: ActionErrorKind::LackBalanceForState { account_id, .. }, .. } + )) if *account_id == new_account_id + )); +} + +/// Test that both storage staking and permanent storage bytes are taken into account for storage allowance. +/// Test that further transfer is prohibited because it would make the storage stake too low. +#[test] +fn storage_staking_and_permanent_storage_bytes_taken_into_account() { + let mut env = setup_env(); + let new_account_id = new_account_id(0); + let new_account = InMemorySigner::from_seed( + new_account_id.clone(), + KeyType::ED25519, + new_account_id.as_str(), + ); + let storage_bytes_required = calculate_named_account_storage_usage(TEST_CONTRACT_SIZE) as u128; + // Just third of the required storage bytes will be covered by permanent storage bytes. + let permanent_storage_bytes = storage_bytes_required / 3; + let nonrefundable_amount = + permanent_storage_bytes * fee_helper().rt_cfg.storage_amount_per_byte(); + // Remaining storage allowance will be provided by storage staking. + let storage_stake = (storage_bytes_required - permanent_storage_bytes) + * fee_helper().rt_cfg.storage_amount_per_byte(); + let tx_result = exec_transfers( + &mut env, + signer(), + new_account_id.clone(), + TransferConfig { + transfers: Transfers { + regular_amount: storage_stake, + nonrefundable_amount, + nonrefundable_transfer_first: false, + }, + account_creation: true, + implicit_account_creation: false, + deploy_contract: true, + }, + ); + tx_result.unwrap().assert_success(); + + // Now we attempt to make a transfer of 1 yoctonear that would result in insufficient balance for state. + let tx_result = regular_transfer(&mut env, new_account, receiver(), 1); + assert!(matches!( + tx_result, + Err(InvalidTxError::LackBalanceForState { signer_id, .. }) if *signer_id == new_account_id + )); +} + +/// Non-refundable transfer amount is burnt. +#[test] +fn non_refundable_transfer_amount_is_burnt() { + let mut env = setup_env(); + let total_supply_before = get_total_supply(&env); + let new_account_id = new_account_id(0); + let nonrefundable_amount = 42 * NEAR_BASE; + let tx_result = exec_transfers( + &mut env, + signer(), + new_account_id, + TransferConfig { + transfers: Transfers { + regular_amount: 0, + nonrefundable_amount, + nonrefundable_transfer_first: true, + }, + account_creation: true, + implicit_account_creation: false, + deploy_contract: false, + }, + ); + tx_result.unwrap().assert_success(); + let total_supply_after = get_total_supply(&env); + let transaction_fee = fee_helper().create_account_transfer_full_key_cost(); + assert_eq!(total_supply_after, total_supply_before - transaction_fee - nonrefundable_amount); +} + +/// Non-refundable amount is burnt even if the receiver is both created and deleted in the single action receipt. +#[test] +fn create_account_nonrefundable_delete_account() { + let mut env = setup_env(); + let mut actions = vec![]; + let new_account_id = new_account_id(0); + let nonrefundable_amount = 42 * NEAR_BASE; + + actions.push(Action::CreateAccount(CreateAccountAction {})); + actions.push(Action::AddKey(Box::new(AddKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, new_account_id.as_str()), + access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, + }))); + actions.push(Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { + deposit: nonrefundable_amount, + })); + actions.push(Action::DeleteAccount(DeleteAccountAction { beneficiary_id: sender() })); + + let total_supply_before = get_total_supply(&env); + let tx_result = execute_transaction_from_actions(&mut env, actions, &signer(), new_account_id); tx_result.unwrap().assert_success(); + // We create and delete account within a single action receipt, thus we remove duplicate `new_action_receipt_cost` from this calculation. + let transaction_fee = fee_helper().create_account_transfer_full_key_cost() + + fee_helper().prepaid_delete_account_cost() + - fee_helper().new_action_receipt_cost(); + let total_supply_after = get_total_supply(&env); + assert_eq!(total_supply_after, total_supply_before - transaction_fee - nonrefundable_amount); } -/// Non-refundable transfer successfully adds non-refundable balance when creating named account. +/// Non-refundable transfer successfully adds permanent storage bytes when creating named account. #[test] fn non_refundable_transfer_create_named_account() { 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 new_account_id = new_account_id(index); let tx_result = exec_transfers( &mut setup_env(), signer(), @@ -368,7 +560,7 @@ fn non_refundable_transfer_create_named_account() { } } -/// Non-refundable transfer successfully adds non-refundable balance when creating NEAR-implicit account. +/// Non-refundable transfer successfully adds permanent storage bytes when creating NEAR-implicit account. #[test] fn non_refundable_transfer_create_near_implicit_account() { for (index, transfers) in TEST_CASES.iter().enumerate() { @@ -401,7 +593,7 @@ fn non_refundable_transfer_create_near_implicit_account() { } } -/// Non-refundable transfer successfully adds non-refundable balance when creating ETH-implicit account. +/// Non-refundable transfer successfully adds permanent storage bytes when creating ETH-implicit account. #[test] fn non_refundable_transfer_create_eth_implicit_account() { for (index, transfers) in TEST_CASES.iter().enumerate() { @@ -453,7 +645,7 @@ fn reject_non_refundable_transfer_existing_account() { assert!(matches!( status, ExecutionStatusView::Failure(TxExecutionError::ActionError( - ActionError { kind: ActionErrorKind::NonRefundableBalanceToExistingAccount { account_id }, .. } + ActionError { kind: ActionErrorKind::NonRefundableTransferToExistingAccount { account_id }, .. } )) if *account_id == receiver(), )); } @@ -468,7 +660,7 @@ fn reject_non_refundable_transfer_existing_account() { #[test] fn reject_non_refundable_transfer_in_older_versions() { let mut env = setup_env_with_protocol_version(Some( - ProtocolFeature::NonRefundableBalance.protocol_version() - 1, + ProtocolFeature::NonrefundableStorage.protocol_version() - 1, )); for transfers in TEST_CASES { let tx_result = exec_transfers( @@ -486,8 +678,8 @@ fn reject_non_refundable_transfer_in_older_versions() { tx_result, Err(InvalidTxError::ActionsValidation( ActionsValidationError::UnsupportedProtocolFeature { - protocol_feature: "NonRefundableBalance".to_string(), - version: ProtocolFeature::NonRefundableBalance.protocol_version() + protocol_feature: "NonrefundableStorage".to_string(), + version: ProtocolFeature::NonrefundableStorage.protocol_version() } )) ); diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 699b8973bd4..8e730c1db1e 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -21,7 +21,7 @@ use near_primitives::transaction::{ }; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{ - AccountId, Balance, BlockHeight, EpochInfoProvider, Gas, TrieCacheMode, + AccountId, Balance, BlockHeight, EpochInfoProvider, Gas, StorageUsage, TrieCacheMode, }; use near_primitives::utils::{account_is_implicit, create_random_seed}; use near_primitives::version::{ @@ -486,12 +486,18 @@ pub(crate) fn action_transfer(account: &mut Account, deposit: Balance) -> Result pub(crate) fn action_nonrefundable_storage_transfer( account: &mut Account, deposit: Balance, + storage_amount_per_byte: Balance, ) -> Result<(), StorageError> { - account.set_nonrefundable(account.nonrefundable().checked_add(deposit).ok_or_else(|| { - StorageError::StorageInconsistentState( - "non-refundable account balance integer overflow".to_string(), - ) - })?); + let permanent_storage_bytes = (deposit / storage_amount_per_byte) as StorageUsage; + account.set_permanent_storage_bytes( + account.permanent_storage_bytes().checked_add(permanent_storage_bytes).ok_or_else( + || { + StorageError::StorageInconsistentState( + "permanent_storage_bytes integer overflow".to_string(), + ) + }, + )?, + ); Ok(()) } @@ -554,12 +560,17 @@ pub(crate) fn action_implicit_account_creation_transfer( deposit: Balance, block_height: BlockHeight, current_protocol_version: ProtocolVersion, - nonrefundable: bool, + nonrefundable_storage_transfer: bool, ) { *actor_id = account_id.clone(); - let (refundable_balance, nonrefundable_balance) = - if nonrefundable { (0, deposit) } else { (deposit, 0) }; + let (amount, permanent_storage_bytes) = if nonrefundable_storage_transfer { + let permanent_storage_bytes = + (deposit / apply_state.config.storage_amount_per_byte()) as StorageUsage; + (0, permanent_storage_bytes) + } else { + (deposit, 0) + }; match account_id.get_account_type() { AccountType::NearImplicitAccount => { @@ -579,9 +590,9 @@ pub(crate) fn action_implicit_account_creation_transfer( let public_key = PublicKey::from_near_implicit_account(account_id).unwrap(); *account = Some(Account::new( - refundable_balance, + amount, 0, - nonrefundable_balance, + permanent_storage_bytes, CryptoHash::default(), fee_config.storage_usage_config.num_bytes_account + public_key.len() as u64 @@ -606,9 +617,9 @@ pub(crate) fn action_implicit_account_creation_transfer( + fee_config.storage_usage_config.num_extra_bytes_record; *account = Some(Account::new( - refundable_balance, + amount, 0, - nonrefundable_balance, + permanent_storage_bytes, *magic_bytes.hash(), storage_usage, current_protocol_version, @@ -1106,7 +1117,7 @@ pub(crate) fn check_account_existence( // purpose. // For implicit accounts creation with non-refundable storage // we require that this is the only action in the receipt. - return Err(ActionErrorKind::NonRefundableBalanceToExistingAccount { + return Err(ActionErrorKind::NonRefundableTransferToExistingAccount { account_id: account_id.clone(), } .into()); diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 87cb5ca1b59..27cfc8c7add 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -78,11 +78,11 @@ fn total_accounts_balance( accounts_ids: &HashSet, ) -> Result { accounts_ids.iter().try_fold(0u128, |accumulator, account_id| { - let (amount, locked, nonrefundable) = match get_account(state, account_id)? { + let (amount, locked) = match get_account(state, account_id)? { None => return Ok(accumulator), - Some(account) => (account.amount(), account.locked(), account.nonrefundable()), + Some(account) => (account.amount(), account.locked()), }; - Ok(safe_add_balance_apply!(accumulator, amount, locked, nonrefundable)) + Ok(safe_add_balance_apply!(accumulator, amount, locked)) }) } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index ecea5668f1d..5801211b84c 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -26,12 +26,12 @@ use near_primitives::receipt::{ 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::NonrefundableStorageTransferAction; use near_primitives::transaction::{ Action, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithId, ExecutionStatus, LogEntry, SignedTransaction, TransferAction, }; -#[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] -use near_primitives::transaction::{DeleteAccountAction, NonrefundableStorageTransferAction}; use near_primitives::trie_key::TrieKey; use near_primitives::types::{ validator_stake::ValidatorStake, AccountId, Balance, BlockHeight, Compute, EpochHeight, @@ -525,10 +525,6 @@ 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 @@ -565,6 +561,9 @@ impl Runtime { result.gas_burnt = exec_fees; // TODO(#8806): Support compute costs for actions. For now they match burnt gas. result.compute_usage = exec_fees; + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let mut nonrefundable_amount_burnt: Balance = 0; + // Executing actions one by one for (action_index, action) in action_receipt.actions.iter().enumerate() { let action_hash = create_action_hash_from_receipt_id( @@ -606,17 +605,12 @@ impl Runtime { 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: _ })) { - // 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_update.nonrefundable(), - )? - } + if let Action::NonrefundableStorageTransfer(NonrefundableStorageTransferAction { + deposit, + }) = action + { + nonrefundable_amount_burnt = safe_add_balance(nonrefundable_amount_burnt, *deposit)? } } @@ -700,6 +694,12 @@ impl Runtime { state_update.rollback(); } }; + // If the receipt was successfully applied, we update `other_burnt_amount` statistic with the non-refundable amount burnt. + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + if result.result.is_ok() { + stats.other_burnt_amount = + safe_add_balance(stats.other_burnt_amount, nonrefundable_amount_burnt)?; + } // If the receipt is a refund, then we consider it free without burnt gas. let gas_burnt: Gas = if receipt.predecessor_id.is_system() { 0 } else { result.gas_burnt }; @@ -1725,7 +1725,11 @@ fn action_transfer_or_implicit_account_creation( if nonrefundable { assert!(cfg!(feature = "protocol_feature_nonrefundable_transfer_nep491")); #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] - action_nonrefundable_storage_transfer(account, deposit)?; + action_nonrefundable_storage_transfer( + account, + deposit, + apply_state.config.storage_amount_per_byte(), + )?; } else { action_transfer(account, deposit)?; } diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 9ccdad3e83f..57c0bdd4b5a 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -45,22 +45,29 @@ pub fn check_storage_stake( runtime_config: &RuntimeConfig, current_protocol_version: ProtocolVersion, ) -> Result<(), StorageStakingError> { - let required_amount = Balance::from(account.storage_usage()) + #[cfg(not(feature = "protocol_feature_nonrefundable_transfer_nep491"))] + let billable_storage_bytes = account.storage_usage(); + #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] + let billable_storage_bytes = + account.storage_usage().saturating_sub(account.permanent_storage_bytes()); + + let required_amount = Balance::from(billable_storage_bytes) .checked_mul(runtime_config.storage_amount_per_byte()) .ok_or_else(|| { - format!("Account's storage_usage {} overflows multiplication", account.storage_usage()) + format!( + "Account's billable storage usage {} overflows multiplication", + billable_storage_bytes + ) }) .map_err(StorageStakingError::StorageError)?; let available_amount = account .amount() .checked_add(account.locked()) - .and_then(|amount| amount.checked_add(account.nonrefundable())) .ok_or_else(|| { format!( - "Account's amount {}, locked {}, and non-refundable {} overflow addition", + "Account's amount {} and locked {} overflow addition", account.amount(), account.locked(), - account.nonrefundable(), ) }) .map_err(StorageStakingError::StorageError)?; @@ -403,7 +410,7 @@ pub fn validate_action( Action::Transfer(_) => Ok(()), #[cfg(feature = "protocol_feature_nonrefundable_transfer_nep491")] Action::NonrefundableStorageTransfer(_) => { - check_feature_enabled(ProtocolFeature::NonRefundableBalance, current_protocol_version) + check_feature_enabled(ProtocolFeature::NonrefundableStorage, current_protocol_version) } Action::Stake(a) => validate_stake_action(a), Action::AddKey(a) => validate_add_key_action(limit_config, a), diff --git a/test-utils/testlib/src/fees_utils.rs b/test-utils/testlib/src/fees_utils.rs index ab5e9bd626b..65a434a68fd 100644 --- a/test-utils/testlib/src/fees_utils.rs +++ b/test-utils/testlib/src/fees_utils.rs @@ -34,6 +34,12 @@ impl FeeHelper { self.gas_to_balance(gas_reward) } + pub fn new_action_receipt_cost(&self) -> Balance { + let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee(); + let send_gas = self.cfg().fee(ActionCosts::new_action_receipt).send_fee(false); + self.gas_to_balance(exec_gas + send_gas) + } + pub fn create_account_cost(&self) -> Balance { let exec_gas = self.cfg().fee(ActionCosts::new_action_receipt).exec_fee() + self.cfg().fee(ActionCosts::create_account).exec_fee(); diff --git a/tools/amend-genesis/src/lib.rs b/tools/amend-genesis/src/lib.rs index 6e9880e4519..b3877ad6683 100644 --- a/tools/amend-genesis/src/lib.rs +++ b/tools/amend-genesis/src/lib.rs @@ -5,7 +5,7 @@ use near_crypto::PublicKey; use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardLayout; use near_primitives::state_record::StateRecord; -use near_primitives::types::{AccountId, AccountInfo}; +use near_primitives::types::{AccountId, AccountInfo, StorageUsage}; use near_primitives::utils; use near_primitives::version::ProtocolVersion; use near_primitives_core::account::{AccessKey, Account}; @@ -51,11 +51,11 @@ impl AccountRecords { fn new( amount: Balance, locked: Balance, - nonrefundable: Balance, + permanent_storage_bytes: StorageUsage, num_bytes_account: u64, ) -> Self { let mut ret = Self::default(); - ret.set_account(amount, locked, nonrefundable, num_bytes_account); + ret.set_account(amount, locked, permanent_storage_bytes, num_bytes_account); ret } @@ -70,14 +70,14 @@ impl AccountRecords { &mut self, amount: Balance, locked: Balance, - nonrefundable: Balance, + permanent_storage_bytes: StorageUsage, num_bytes_account: u64, ) { assert!(self.account.is_none()); let account = Account::new( amount, locked, - nonrefundable, + permanent_storage_bytes, CryptoHash::default(), num_bytes_account, PROTOCOL_VERSION, @@ -200,7 +200,7 @@ fn parse_extra_records( let r = AccountRecords::new( account.amount(), account.locked(), - account.nonrefundable(), + account.permanent_storage_bytes(), num_bytes_account, ); e.insert(r); @@ -217,7 +217,7 @@ fn parse_extra_records( r.set_account( account.amount(), account.locked(), - account.nonrefundable(), + account.permanent_storage_bytes(), num_bytes_account, ); } @@ -472,12 +472,12 @@ 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; + // `permanent_storage_bytes` can be implemented if this is required in state records. + let permanent_storage_bytes = 0; let account = Account::new( *amount, *locked, - nonrefundable_balance, + permanent_storage_bytes, CryptoHash::default(), *storage_usage, PROTOCOL_VERSION,