From 8f96389d84da7867df214c2f31e814fa60f5e3a2 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 19 Jan 2023 15:10:14 +0100 Subject: [PATCH] feat: draft implementation of NEP-366 (fork) (#8385) A fork of https://github.com/near/nearcore/pull/7497 Nearcore code owners currently don't have permissions to change the original PR, so we will add the necessary changes here before merging. This is also merged with the current master. --- chain/chain/src/tests/simple_chain.rs | 4 +- chain/jsonrpc/res/rpc_errors_schema.json | 56 +- chain/rosetta-rpc/Cargo.toml | 8 + chain/rosetta-rpc/src/adapters/mod.rs | 2 + core/primitives-core/Cargo.toml | 3 +- core/primitives-core/src/config.rs | 2 + core/primitives-core/src/parameter.rs | 6 + core/primitives-core/src/runtime/fees.rs | 9 + core/primitives/Cargo.toml | 4 + .../res/runtime_configs/parameters.yaml | 4 + .../runtime_configs/parameters_testnet.yaml | 4 + core/primitives/src/errors.rs | 30 + ..._runtime__config_store__tests__0.json.snap | 2 +- ...runtime__config_store__tests__42.json.snap | 2 +- ...runtime__config_store__tests__48.json.snap | 2 +- ...runtime__config_store__tests__49.json.snap | 2 +- ...runtime__config_store__tests__50.json.snap | 2 +- ...runtime__config_store__tests__52.json.snap | 2 +- ...runtime__config_store__tests__53.json.snap | 2 +- ...runtime__config_store__tests__57.json.snap | 2 +- ...__config_store__tests__testnet_0.json.snap | 2 +- ..._config_store__tests__testnet_42.json.snap | 2 +- ..._config_store__tests__testnet_48.json.snap | 2 +- ..._config_store__tests__testnet_49.json.snap | 2 +- ..._config_store__tests__testnet_50.json.snap | 2 +- ..._config_store__tests__testnet_52.json.snap | 2 +- ..._config_store__tests__testnet_53.json.snap | 2 +- ..._config_store__tests__testnet_57.json.snap | 2 +- core/primitives/src/transaction.rs | 202 ++++- core/primitives/src/version.rs | 6 +- core/primitives/src/views.rs | 30 + nearcore/Cargo.toml | 9 +- neard/Cargo.toml | 1 + runtime/runtime-params-estimator/Cargo.toml | 4 +- runtime/runtime-params-estimator/src/cost.rs | 4 +- .../src/costs_to_runtime_config.rs | 2 + runtime/runtime/Cargo.toml | 1 + runtime/runtime/src/actions.rs | 851 ++++++++++++++++++ runtime/runtime/src/balance_checker.rs | 10 +- runtime/runtime/src/config.rs | 137 ++- runtime/runtime/src/lib.rs | 22 +- runtime/runtime/src/verifier.rs | 74 ++ 42 files changed, 1482 insertions(+), 35 deletions(-) diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index 8dc269a8608..b7a9795440f 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -43,7 +43,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, @"HTpETHnBkxcX1h3eD87uC5YP5nV66E6UYPrJGnQHuRqt"); + insta::assert_display_snapshot!(hash, @"96KiRJdbMN8A9cFPXarZdaRQ8U2HvYcrGTGC8a4EgFzM"); } else { insta::assert_display_snapshot!(hash, @"7r5VSLXhkxHHEeiAAPQbKPGv3rr877obehGYwPbKZMA7"); } @@ -73,7 +73,7 @@ fn build_chain() { let hash = chain.head().unwrap().last_block_hash; if cfg!(feature = "nightly") { - insta::assert_display_snapshot!(hash, @"HyDYbjs5tgeEDf1N1XB4m312VdCeKjHqeGQ7dc7Lqwv8"); + insta::assert_display_snapshot!(hash, @"4eW4jvyu1Ek6WmY3EuUoFFkrascC7svRww5UcZbNMkUf"); } else { insta::assert_display_snapshot!(hash, @"9772sSKzm1eGPV3pRi17YaZkotrcN6dAkJUn226CopTm"); } diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json index 4401321ff69..4606910cb67 100644 --- a/chain/jsonrpc/res/rpc_errors_schema.json +++ b/chain/jsonrpc/res/rpc_errors_schema.json @@ -460,7 +460,13 @@ "FunctionCallError", "NewReceiptValidationError", "OnlyImplicitAccountCreationAllowed", - "DeleteAccountWithLargeState" + "DeleteAccountWithLargeState", + "DelegateActionInvalidSignature", + "DelegateActionSenderDoesNotMatchTxReceiver", + "DelegateActionExpired", + "DelegateActionAccessKeyError", + "DelegateActionInvalidNonce", + "DelegateActionNonceTooLarge" ], "props": { "index": "" @@ -480,7 +486,9 @@ "FunctionCallMethodNameLengthExceeded", "FunctionCallArgumentsLengthExceeded", "UnsuitableStakingKey", - "FunctionCallZeroAttachedGas" + "FunctionCallZeroAttachedGas", + "DelegateActionCantContainNestedOne", + "DelegateActionMustBeOnlyOne" ], "props": {} }, @@ -556,6 +564,50 @@ "registrar_account_id": "" } }, + "DelegateActionCantContainNestedOne": { + "name": "DelegateActionCantContainNestedOne", + "subtypes": [], + "props": {} + }, + "DelegateActionExpired": { + "name": "DelegateActionExpired", + "subtypes": [], + "props": {} + }, + "DelegateActionInvalidNonce": { + "name": "DelegateActionInvalidNonce", + "subtypes": [], + "props": { + "ak_nonce": "", + "delegate_nonce": "" + } + }, + "DelegateActionInvalidSignature": { + "name": "DelegateActionInvalidSignature", + "subtypes": [], + "props": {} + }, + "DelegateActionMustBeOnlyOne": { + "name": "DelegateActionMustBeOnlyOne", + "subtypes": [], + "props": {} + }, + "DelegateActionNonceTooLarge": { + "name": "DelegateActionNonceTooLarge", + "subtypes": [], + "props": { + "delegate_nonce": "", + "upper_bound": "" + } + }, + "DelegateActionSenderDoesNotMatchTxReceiver": { + "name": "DelegateActionSenderDoesNotMatchTxReceiver", + "subtypes": [], + "props": { + "receiver_id": "", + "sender_id": "" + } + }, "DeleteAccountStaking": { "name": "DeleteAccountStaking", "subtypes": [], diff --git a/chain/rosetta-rpc/Cargo.toml b/chain/rosetta-rpc/Cargo.toml index 7876f388876..69f1129082f 100644 --- a/chain/rosetta-rpc/Cargo.toml +++ b/chain/rosetta-rpc/Cargo.toml @@ -36,3 +36,11 @@ near-o11y = { path = "../../core/o11y" } [dev-dependencies] insta = "1" near-actix-test-utils = { path = "../../test-utils/actix-test-utils" } + +[features] +protocol_feature_nep366_delegate_action = [ + "near-primitives/protocol_feature_nep366_delegate_action" +] +nightly = [ + "protocol_feature_nep366_delegate_action" +] \ No newline at end of file diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index e93dbec7fef..09716d7ec5a 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -419,6 +419,8 @@ impl From for Vec { ); operations.push(deploy_contract_operation); } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + near_primitives::transaction::Action::Delegate(_) => todo!(), } } operations diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index 617391b1602..52e6e197bf1 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -35,4 +35,5 @@ insta.workspace = true [features] default = [] protocol_feature_ed25519_verify = [] -nightly = ["protocol_feature_ed25519_verify"] +protocol_feature_nep366_delegate_action = [] +nightly = ["protocol_feature_ed25519_verify", "protocol_feature_nep366_delegate_action"] diff --git a/core/primitives-core/src/config.rs b/core/primitives-core/src/config.rs index 7e230769592..0190b89b24a 100644 --- a/core/primitives-core/src/config.rs +++ b/core/primitives-core/src/config.rs @@ -473,6 +473,8 @@ pub enum ActionCosts { new_action_receipt = 12, new_data_receipt_base = 13, new_data_receipt_byte = 14, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + delegate = 15, } impl ExtCosts { diff --git a/core/primitives-core/src/parameter.rs b/core/primitives-core/src/parameter.rs index f54e9e493dc..0bb7117c10b 100644 --- a/core/primitives-core/src/parameter.rs +++ b/core/primitives-core/src/parameter.rs @@ -79,6 +79,9 @@ pub enum Parameter { ActionDeleteKeySendSir, ActionDeleteKeySendNotSir, ActionDeleteKeyExecution, + ActionDelegateSendSir, + ActionDelegateSendNotSir, + ActionDelegateExecution, // Smart contract dynamic gas costs WasmRegularOpCost, @@ -205,6 +208,7 @@ pub enum FeeParameter { ActionAddFunctionCallKey, ActionAddFunctionCallKeyPerByte, ActionDeleteKey, + ActionDelegate, } impl Parameter { @@ -250,6 +254,8 @@ impl From for FeeParameter { match other { ActionCosts::create_account => Self::ActionCreateAccount, ActionCosts::delete_account => Self::ActionDeleteAccount, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ActionCosts::delegate => Self::ActionDelegate, ActionCosts::deploy_contract_base => Self::ActionDeployContract, ActionCosts::deploy_contract_byte => Self::ActionDeployContractPerByte, ActionCosts::function_call_base => Self::ActionFunctionCall, diff --git a/core/primitives-core/src/runtime/fees.rs b/core/primitives-core/src/runtime/fees.rs index 29acd6b3182..8432ea059e7 100644 --- a/core/primitives-core/src/runtime/fees.rs +++ b/core/primitives-core/src/runtime/fees.rs @@ -108,6 +108,9 @@ pub struct ActionCreationConfig { /// Base cost of deleting an account. pub delete_account_cost: Fee, + + /// Base cost of a delegate action + pub delegate_cost: Fee, } /// Describes the cost of creating an access key. @@ -220,6 +223,12 @@ impl RuntimeFeesConfig { send_not_sir: 59357464, execution: 59357464, }, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ActionCosts::delegate => Fee { + send_sir: 2319861500000, + send_not_sir: 2319861500000, + execution: 2319861500000, + }, }, } } diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 2a6f756c41a..7ab3a5d001c 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -51,12 +51,16 @@ protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_ed25519_verify = [ "near-primitives-core/protocol_feature_ed25519_verify" ] +protocol_feature_nep366_delegate_action = [ + "near-primitives-core/protocol_feature_nep366_delegate_action" +] nightly = [ "nightly_protocol", "protocol_feature_fix_staking_threshold", "protocol_feature_fix_contract_loading_cost", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_ed25519_verify", + "protocol_feature_nep366_delegate_action", ] nightly_protocol = [] diff --git a/core/primitives/res/runtime_configs/parameters.yaml b/core/primitives/res/runtime_configs/parameters.yaml index 8fb5fb4a8cf..139c8dbeb42 100644 --- a/core/primitives/res/runtime_configs/parameters.yaml +++ b/core/primitives/res/runtime_configs/parameters.yaml @@ -70,6 +70,10 @@ action_add_function_call_key_per_byte_execution: 1_925_331 action_delete_key_send_sir: 94_946_625_000 action_delete_key_send_not_sir: 94_946_625_000 action_delete_key_execution: 94_946_625_000 +# TODO: place-holder values, needs estimation, tracked in #8114 +action_delegate_send_sir: 2_319_861_500_000 +action_delegate_send_not_sir: 2_319_861_500_000 +action_delegate_execution: 2_319_861_500_000 # Smart contract dynamic gas costs wasm_regular_op_cost: 3_856_371 diff --git a/core/primitives/res/runtime_configs/parameters_testnet.yaml b/core/primitives/res/runtime_configs/parameters_testnet.yaml index 0fa409d435c..489af03f731 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.yaml +++ b/core/primitives/res/runtime_configs/parameters_testnet.yaml @@ -66,6 +66,10 @@ action_add_function_call_key_per_byte_execution: 1_925_331 action_delete_key_send_sir: 94_946_625_000 action_delete_key_send_not_sir: 94_946_625_000 action_delete_key_execution: 94_946_625_000 +# TODO: place-holder values, needs estimation, tracked in #8114 +action_delegate_send_sir: 2_319_861_500_000 +action_delegate_send_not_sir: 2_319_861_500_000 +action_delegate_execution: 2_319_861_500_000 # Smart contract dynamic gas costs wasm_regular_op_cost: 3_856_371 diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 8354409b1f7..d5a98b8cb3f 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -198,6 +198,10 @@ pub enum ActionsValidationError { UnsuitableStakingKey { public_key: PublicKey }, /// The attached amount of gas in a FunctionCall action has to be a positive number. FunctionCallZeroAttachedGas, + /// DelegateAction actions contain another DelegateAction. This is not allowed. + DelegateActionCantContainNestedOne, + /// There should be the only one DelegateAction + DelegateActionMustBeOnlyOne, } /// Describes the error for validating a receipt. @@ -314,6 +318,14 @@ impl Display for ActionsValidationError { f, "The attached amount of gas in a FunctionCall action has to be a positive number", ), + ActionsValidationError::DelegateActionCantContainNestedOne => write!( + f, + "DelegateAction must not contain another DelegateAction" + ), + ActionsValidationError::DelegateActionMustBeOnlyOne => write!( + f, + "The actions can contain the ony one DelegateAction" + ) } } } @@ -397,6 +409,18 @@ pub enum ActionErrorKind { OnlyImplicitAccountCreationAllowed { account_id: AccountId }, /// Delete account whose state is large is temporarily banned. DeleteAccountWithLargeState { account_id: AccountId }, + /// Signature does not match the provided actions and given signer public key. + DelegateActionInvalidSignature, + /// Receiver of the transaction doesn't match Sender of the delegate action + DelegateActionSenderDoesNotMatchTxReceiver { sender_id: AccountId, receiver_id: AccountId }, + /// Delegate action has expired. `max_block_height` is less than actual block height. + DelegateActionExpired, + /// The given public key doesn't exist for Sender account + DelegateActionAccessKeyError(InvalidAccessKeyError), + /// DelegateAction nonce must be greater sender[public_key].nonce + 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 }, } impl From for ActionError { @@ -707,6 +731,12 @@ impl Display for ActionErrorKind { ActionErrorKind::InsufficientStake { account_id, stake, minimum_stake } => write!(f, "Account {} tries to stake {} but minimum required stake is {}", account_id, stake, minimum_stake), ActionErrorKind::OnlyImplicitAccountCreationAllowed { account_id } => write!(f, "CreateAccount action is called on hex-characters account of length 64 {}", account_id), ActionErrorKind::DeleteAccountWithLargeState { account_id } => write!(f, "The state of account {} is too large and therefore cannot be deleted", account_id), + ActionErrorKind::DelegateActionInvalidSignature => write!(f, "DelegateAction is not signed with the given public key"), + ActionErrorKind::DelegateActionSenderDoesNotMatchTxReceiver { sender_id, receiver_id } => write!(f, "Transaction receiver {} doesn't match DelegateAction sender {}", receiver_id, sender_id), + ActionErrorKind::DelegateActionExpired => write!(f, "DelegateAction has expired"), + 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), } } } diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap index 27967578168..e2b56d1e207 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__0.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "100000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap index cdd3a59e144..c55199a66e3 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__42.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap index 9cfea46f7ce..f5f5c3c9f9c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__48.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap index c3c4c0abf98..cf2cc240f9f 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__49.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap index 07bb9527bde..2ec6212599c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__50.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap index 080e25788d1..7db6d711dc7 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__52.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap index 541b8e67187..a499aa7e8bc 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__53.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap index 529a8593a87..a96cd4b18af 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__57.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap index 27967578168..e2b56d1e207 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_0.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "100000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap index cdd3a59e144..c55199a66e3 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_42.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap index 9cfea46f7ce..f5f5c3c9f9c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_48.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap index c3c4c0abf98..cf2cc240f9f 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_49.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap index 07bb9527bde..2ec6212599c 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_50.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap index 080e25788d1..7db6d711dc7 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_52.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap index 541b8e67187..a499aa7e8bc 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_53.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap index 529a8593a87..a96cd4b18af 100644 --- a/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap +++ b/core/primitives/src/runtime/snapshots/near_primitives__runtime__config_store__tests__testnet_57.json.snap @@ -1,6 +1,6 @@ --- source: core/primitives/src/runtime/config_store.rs -expression: store.get_config(*version) +expression: config_view --- { "storage_amount_per_byte": "10000000000000000000", diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 40f32a32d4d..fa6e6112568 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -1,8 +1,10 @@ use std::borrow::Borrow; use std::fmt; use std::hash::{Hash, Hasher}; +use std::io::{Error, ErrorKind}; use borsh::{BorshDeserialize, BorshSerialize}; +use near_primitives_core::types::BlockHeight; use serde::{Deserialize, Serialize}; use near_crypto::{PublicKey, Signature}; @@ -18,6 +20,9 @@ use crate::types::{AccountId, Balance, Gas, Nonce}; pub type LogEntry = String; +// This is an index number of Action::Delegate in Action enumeration +const ACTION_DELEGATE_NUMBER: u8 = 8; + #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Transaction { /// An account on which behalf transaction is signed @@ -68,6 +73,8 @@ pub enum Action { AddKey(AddKeyAction), DeleteKey(DeleteKeyAction), DeleteAccount(DeleteAccountAction), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Delegate(SignedDelegateAction), } impl Action { @@ -210,6 +217,95 @@ impl From for Action { } } +/// This is Action which mustn't contain DelegateAction. +// This struct is needed to avoid the recursion when Action/DelegateAction is deserialized. +#[derive(Serialize, BorshSerialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct NonDelegateAction(pub Action); + +impl From for Action { + fn from(action: NonDelegateAction) -> Self { + action.0 + } +} + +impl borsh::de::BorshDeserialize for NonDelegateAction { + fn deserialize(buf: &mut &[u8]) -> ::core::result::Result { + if buf.is_empty() { + return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to deserialize DelegateAction", + )); + } + match buf[0] { + ACTION_DELEGATE_NUMBER => Err(Error::new( + ErrorKind::InvalidInput, + "DelegateAction mustn't contain a nested one", + )), + _ => Ok(Self(borsh::BorshDeserialize::deserialize(buf)?)), + } + } +} + +/// This action allows to execute the inner actions behalf of the defined sender. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct DelegateAction { + /// Signer of the delegated actions + pub sender_id: AccountId, + /// Receiver of the delegated actions. + pub receiver_id: AccountId, + /// List of actions to be executed. + pub actions: Vec, + /// Nonce to ensure that the same delegate action is not sent twice by a relayer and should match for given account's `public_key`. + /// After this action is processed it will increment. + pub nonce: Nonce, + /// The maximal height of the block in the blockchain below which the given DelegateAction is valid. + pub max_block_height: BlockHeight, + /// Public key that is used to sign this delegated action. + pub public_key: PublicKey, +} + +#[cfg_attr(feature = "protocol_feature_nep366_delegate_action", derive(BorshDeserialize))] +#[derive(BorshSerialize, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct SignedDelegateAction { + pub delegate_action: DelegateAction, + pub signature: Signature, +} + +#[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] +impl borsh::de::BorshDeserialize for SignedDelegateAction { + fn deserialize(_buf: &mut &[u8]) -> ::core::result::Result { + return Err(Error::new(ErrorKind::InvalidInput, "Delegate action isn't supported")); + } +} + +impl SignedDelegateAction { + pub fn verify(&self) -> bool { + let delegate_action = &self.delegate_action; + let hash = delegate_action.get_hash(); + let public_key = &delegate_action.public_key; + + self.signature.verify(hash.as_ref(), public_key) + } +} + +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +impl From for Action { + fn from(delegate_action: SignedDelegateAction) -> Self { + Self::Delegate(delegate_action) + } +} + +impl DelegateAction { + pub fn get_actions(&self) -> Vec { + self.actions.iter().map(|a| a.clone().into()).collect() + } + + pub fn get_hash(&self) -> CryptoHash { + let bytes = self.try_to_vec().expect("Failed to deserialize"); + hash(&bytes) + } +} + #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Eq, Debug, Clone)] #[borsh_init(init)] pub struct SignedTransaction { @@ -446,13 +542,23 @@ pub struct ExecutionOutcomeWithProof { } #[cfg(test)] mod tests { + use super::*; + use crate::account::{AccessKeyPermission, FunctionCallPermission}; use borsh::BorshDeserialize; - use near_crypto::{InMemorySigner, KeyType, Signature, Signer}; - use crate::account::{AccessKeyPermission, FunctionCallPermission}; - - use super::*; + /// A serialized `Action::Delegate(SignedDelegateAction)` for testing. + /// + /// We want this to be parseable and accepted by protocol versions with meta + /// transactions enabled. But it should fail either in parsing or in + /// validation when this is included in a receipt for a block of an earlier + /// version. For now, it just fails to parse, as a test below checks. + const DELEGATE_ACTION_HEX: &str = concat!( + "0803000000616161030000006262620100000000010000000000000002000000000000", + "0000000000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000" + ); #[test] fn test_verify_transaction() { @@ -550,4 +656,92 @@ mod tests { outcome.to_hashes() ); } + + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn create_delegate_action(actions: Vec) -> Action { + Action::Delegate(SignedDelegateAction { + delegate_action: DelegateAction { + sender_id: "aaa".parse().unwrap(), + receiver_id: "bbb".parse().unwrap(), + actions: actions.iter().map(|a| NonDelegateAction(a.clone())).collect(), + nonce: 1, + max_block_height: 2, + public_key: PublicKey::empty(KeyType::ED25519), + }, + signature: Signature::empty(KeyType::ED25519), + }) + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_deserialization() { + // Expected an error. Buffer is empty + assert_eq!( + NonDelegateAction::try_from_slice(Vec::new().as_ref()).map_err(|e| e.kind()), + Err(ErrorKind::InvalidInput) + ); + + let delegate_action = create_delegate_action(Vec::::new()); + let serialized_non_delegate_action = + create_delegate_action(vec![delegate_action]).try_to_vec().expect("Expect ok"); + + // Expected Action::Delegate has not been moved in enum Action + assert_eq!(serialized_non_delegate_action[0], ACTION_DELEGATE_NUMBER); + + // Expected a nested DelegateAction error + assert_eq!( + NonDelegateAction::try_from_slice(&serialized_non_delegate_action) + .map_err(|e| e.kind()), + Err(ErrorKind::InvalidInput) + ); + + let delegate_action = + create_delegate_action(vec![Action::CreateAccount(CreateAccountAction {})]); + let serialized_delegate_action = delegate_action.try_to_vec().expect("Expect ok"); + + // Valid action + assert_eq!( + Action::try_from_slice(&serialized_delegate_action).expect("Expect ok"), + delegate_action + ); + } + + /// Check that we will not accept delegate actions with the feature + /// disabled. + /// + /// This test is to ensure that while working on meta transactions, we don't + /// accientally start accepting delegate actions in receipts. Otherwise, a + /// malicious validator could create receipts that include delegate actions + /// and other nodes will accept such a receipt. + /// + /// TODO: Before stabilizing "protocol_feature_nep366_delegate_action" we + /// have to replace this rest with a test that checks that we discard + /// delegate actions for earlier versions somewhere in validation. + #[test] + #[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] + fn test_delegate_action_deserialization() { + let serialized_delegate_action = hex::decode(DELEGATE_ACTION_HEX).expect("invalid hex"); + + // DelegateAction isn't supported + assert_eq!( + Action::try_from_slice(&serialized_delegate_action).map_err(|e| e.kind()), + Err(ErrorKind::InvalidInput) + ); + } + + /// Check that the hard-coded delegate action is valid. + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_deserialization_hard_coded() { + let serialized_delegate_action = hex::decode(DELEGATE_ACTION_HEX).expect("invalid hex"); + // The hex data is the same as the one we create below. + let delegate_action = + create_delegate_action(vec![Action::CreateAccount(CreateAccountAction {})]); + + // Valid action + assert_eq!( + Action::try_from_slice(&serialized_delegate_action).expect("Expect ok"), + delegate_action + ); + } } diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index dbb7b6e23e5..4baca61f4df 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -147,6 +147,8 @@ pub enum ProtocolFeature { Ed25519Verify, #[cfg(feature = "protocol_feature_reject_blocks_with_outdated_protocol_version")] RejectBlocksWithOutdatedProtocolVersions, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + DelegateAction, } /// Both, outgoing and incoming tcp connections to peers, will be rejected if `peer's` @@ -161,7 +163,7 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 58; /// 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. - 132 + 133 } else { // Enable all stable features. STABLE_PROTOCOL_VERSION @@ -234,6 +236,8 @@ impl ProtocolFeature { ProtocolFeature::Ed25519Verify => 131, #[cfg(feature = "protocol_feature_reject_blocks_with_outdated_protocol_version")] ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => 132, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ProtocolFeature::DelegateAction => 133, } } } diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index ccac1d4f5c7..5708378e891 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -53,6 +53,9 @@ use crate::types::{ use crate::version::{ProtocolVersion, Version}; use validator_stake_view::ValidatorStakeView; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use crate::transaction::{DelegateAction, SignedDelegateAction}; + /// A view of the account #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct AccountView { @@ -1073,6 +1076,11 @@ pub enum ActionView { DeleteAccount { beneficiary_id: AccountId, }, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Delegate { + delegate_action: DelegateAction, + signature: Signature, + }, } impl From for ActionView { @@ -1101,6 +1109,11 @@ impl From for ActionView { Action::DeleteAccount(action) => { ActionView::DeleteAccount { beneficiary_id: action.beneficiary_id } } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Action::Delegate(action) => ActionView::Delegate { + delegate_action: action.delegate_action, + signature: action.signature, + }, } } } @@ -1130,6 +1143,13 @@ impl TryFrom for Action { ActionView::DeleteAccount { beneficiary_id } => { Action::DeleteAccount(DeleteAccountAction { beneficiary_id }) } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ActionView::Delegate { delegate_action, signature } => { + Action::Delegate(SignedDelegateAction { + delegate_action: delegate_action, + signature, + }) + } }) } } @@ -2109,6 +2129,12 @@ pub struct ActionCreationConfigView { /// Base cost of deleting an account. pub delete_account_cost: Fee, + + /// Base cost for processing a delegate action. + /// + /// This is on top of the costs for the actions inside the delegate action. + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + pub delegate_cost: Fee, } /// Describes the cost of creating an access key. @@ -2174,6 +2200,8 @@ impl From for RuntimeConfigView { }, delete_key_cost: config.fees.fee(ActionCosts::delete_key).clone(), delete_account_cost: config.fees.fee(ActionCosts::delete_account).clone(), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + delegate_cost: config.fees.fee(ActionCosts::delegate).clone(), }, storage_usage_config: StorageUsageConfigView { num_bytes_account: config.fees.storage_usage_config.num_bytes_account, @@ -2218,6 +2246,8 @@ impl From for RuntimeConfig { action_fees: enum_map::enum_map! { ActionCosts::create_account => config.transaction_costs.action_creation_config.create_account_cost.clone(), ActionCosts::delete_account => config.transaction_costs.action_creation_config.delete_account_cost.clone(), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ActionCosts::delegate => config.transaction_costs.action_creation_config.delegate_cost.clone(), ActionCosts::deploy_contract_base => config.transaction_costs.action_creation_config.deploy_contract_cost.clone(), ActionCosts::deploy_contract_byte => config.transaction_costs.action_creation_config.deploy_contract_cost_per_byte.clone(), ActionCosts::function_call_base => config.transaction_costs.action_creation_config.function_call_cost.clone(), diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index eda7102ee5c..39c36e8bbbb 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -105,7 +105,12 @@ protocol_feature_fix_staking_threshold = [ protocol_feature_fix_contract_loading_cost = [ "near-vm-runner/protocol_feature_fix_contract_loading_cost", ] -protocol_feature_flat_state = ["near-client/protocol_feature_flat_state", "near-store/protocol_feature_flat_state", "near-chain/protocol_feature_flat_state", "node-runtime/protocol_feature_flat_state"] +protocol_feature_flat_state = ["near-store/protocol_feature_flat_state", "near-chain/protocol_feature_flat_state", "node-runtime/protocol_feature_flat_state"] +protocol_feature_nep366_delegate_action = [ + "node-runtime/protocol_feature_nep366_delegate_action", + "near-primitives/protocol_feature_nep366_delegate_action", + "near-rosetta-rpc/protocol_feature_nep366_delegate_action", +] nightly = [ "nightly_protocol", @@ -113,9 +118,11 @@ nightly = [ "near-client/nightly", "near-epoch-manager/nightly", "near-store/nightly", + "near-rosetta-rpc/nightly", "protocol_feature_fix_staking_threshold", "protocol_feature_fix_contract_loading_cost", "protocol_feature_flat_state", + "protocol_feature_nep366_delegate_action", ] nightly_protocol = [ "near-primitives/nightly_protocol", diff --git a/neard/Cargo.toml b/neard/Cargo.toml index b9b36383265..3ad1513768e 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -63,6 +63,7 @@ rosetta_rpc = ["nearcore/rosetta_rpc"] json_rpc = ["nearcore/json_rpc"] protocol_feature_fix_staking_threshold = ["nearcore/protocol_feature_fix_staking_threshold"] protocol_feature_flat_state = ["nearcore/protocol_feature_flat_state"] +protocol_feature_nep366_delegate_action = ["nearcore/protocol_feature_nep366_delegate_action"] cold_store = ["nearcore/cold_store", "near-store/cold_store", "near-state-viewer/cold_store", "near-cold-store-tool/cold_store"] nightly = [ diff --git a/runtime/runtime-params-estimator/Cargo.toml b/runtime/runtime-params-estimator/Cargo.toml index 368ec24d863..875a34b609b 100644 --- a/runtime/runtime-params-estimator/Cargo.toml +++ b/runtime/runtime-params-estimator/Cargo.toml @@ -68,7 +68,8 @@ nightly = [ nightly_protocol = [ "near-primitives/nightly_protocol", "near-test-contracts/nightly", - "protocol_feature_ed25519_verify" + "protocol_feature_ed25519_verify", + "protocol_feature_nep366_delegate_action", ] sandbox = ["node-runtime/sandbox"] io_trace = ["near-store/io_trace", "near-o11y/io_trace", "near-vm-logic/io_trace"] @@ -76,3 +77,4 @@ protocol_feature_ed25519_verify = [ "near-vm-logic/protocol_feature_ed25519_verify", "near-vm-runner/protocol_feature_ed25519_verify" ] +protocol_feature_nep366_delegate_action = [] diff --git a/runtime/runtime-params-estimator/src/cost.rs b/runtime/runtime-params-estimator/src/cost.rs index bc4e1ff3c48..c64389ceec0 100644 --- a/runtime/runtime-params-estimator/src/cost.rs +++ b/runtime/runtime-params-estimator/src/cost.rs @@ -199,7 +199,9 @@ pub enum Cost { ActionDeleteAccountSendNotSir, ActionDeleteAccountSendSir, ActionDeleteAccountExec, - + /// Estimates `action_creation_config.delegate_cost` which is charged + /// for `DelegateAction` actions. + ActionDelegate, /// Estimates `wasm_config.ext_costs.base` which is intended to be charged /// once on every host function call. However, this is currently /// inconsistent. First, we do not charge on Math API methods (`sha256`, diff --git a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs index 91b146ab22e..83398c9d675 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -54,6 +54,8 @@ fn runtime_fees_config(cost_table: &CostTable) -> anyhow::Result fee(Cost::ActionCreateAccount)?, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + ActionCosts::delegate => fee(Cost::ActionDelegate)?, ActionCosts::delete_account => fee(Cost::ActionDeleteAccount)?, ActionCosts::deploy_contract_base => fee(Cost::ActionDeployContractBase)?, ActionCosts::deploy_contract_byte => fee(Cost::ActionDeployContractPerByte)?, diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 08f6cd99f64..e0656c80d6e 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -36,6 +36,7 @@ default = [] dump_errors_schema = ["near-vm-errors/dump_errors_schema"] protocol_feature_flat_state = ["near-store/protocol_feature_flat_state", "near-vm-logic/protocol_feature_flat_state"] no_cpu_compatibility_checks = ["near-vm-runner/no_cpu_compatibility_checks"] +protocol_feature_nep366_delegate_action = [] no_cache = [ "near-vm-runner/no_cache", diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 59c93618da3..0cad3370edf 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -35,6 +35,17 @@ use near_vm_logic::types::PromiseResult; use near_vm_logic::{VMContext, VMOutcome}; use near_vm_runner::precompile_contract; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use crate::config::{total_prepaid_exec_fees, total_prepaid_gas, total_prepaid_send_fees}; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_primitives::errors::InvalidAccessKeyError; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_primitives::transaction::{DelegateAction, SignedDelegateAction}; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_primitives::types::Gas; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_vm_logic::ActionCosts; + /// Runs given function call with given context / apply state. pub(crate) fn execute_function_call( apply_state: &ApplyState, @@ -624,6 +635,221 @@ pub(crate) fn action_add_key( Ok(()) } +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +pub(crate) fn apply_delegate_action( + state_update: &mut TrieUpdate, + apply_state: &ApplyState, + action_receipt: &ActionReceipt, + sender_id: &AccountId, + signed_delegate_action: &SignedDelegateAction, + result: &mut ActionResult, +) -> Result<(), RuntimeError> { + let delegate_action = &signed_delegate_action.delegate_action; + + if !signed_delegate_action.verify() { + result.result = Err(ActionErrorKind::DelegateActionInvalidSignature.into()); + return Ok(()); + } + if apply_state.block_height > delegate_action.max_block_height { + result.result = Err(ActionErrorKind::DelegateActionExpired.into()); + return Ok(()); + } + if delegate_action.sender_id.as_str() != sender_id.as_str() { + result.result = Err(ActionErrorKind::DelegateActionSenderDoesNotMatchTxReceiver { + sender_id: delegate_action.sender_id.clone(), + receiver_id: sender_id.clone(), + } + .into()); + return Ok(()); + } + + validate_delegate_action_key(state_update, apply_state, delegate_action, result)?; + if result.result.is_err() { + // Validation failed. Need to return Ok() because this is not a runtime error. + // "result.result" will be return to the User as the action execution result. + return Ok(()); + } + + // Generate a new receipt from DelegateAction. + let new_receipt = Receipt { + predecessor_id: sender_id.clone(), + receiver_id: delegate_action.receiver_id.clone(), + receipt_id: CryptoHash::default(), + + receipt: ReceiptEnum::Action(ActionReceipt { + signer_id: action_receipt.signer_id.clone(), + signer_public_key: action_receipt.signer_public_key.clone(), + gas_price: action_receipt.gas_price, + output_data_receivers: vec![], + input_data_ids: vec![], + actions: delegate_action.get_actions(), + }), + }; + + // Note, Relayer prepaid all fees and all things required by actions: attached deposits and attached gas. + // If something goes wrong, deposit is refunded to the predecessor, this is sender_id/Sender in DelegateAction. + // Gas is refunded to the signer, this is Relayer. + // Some contracts refund the deposit. Usually they refund the deposit to the predecessor and this is sender_id/Sender from DelegateAction. + // Therefore Relayer should verify DelegateAction before submitting it because it spends the attached deposit. + + let prepaid_send_fees = total_prepaid_send_fees( + &apply_state.config.fees, + &action_receipt.actions, + apply_state.current_protocol_version, + )?; + let required_gas = receipt_required_gas(apply_state, &new_receipt)?; + // This gas will be burnt by the receiver of the created receipt, + result.gas_used = safe_add_gas(result.gas_used, required_gas)?; + // This gas was prepaid on Relayer shard. Need to burn it because the receipt is going to be sent. + // gas_used is incremented because otherwise the gas will be refunded. Refund function checks only gas_used. + result.gas_used = safe_add_gas(result.gas_used, prepaid_send_fees)?; + result.gas_burnt = safe_add_gas(result.gas_burnt, prepaid_send_fees)?; + result.new_receipts.push(new_receipt); + + Ok(()) +} + +/// Returns Gas amount is required to execute Receipt and all actions it contains +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +fn receipt_required_gas(apply_state: &ApplyState, receipt: &Receipt) -> Result { + Ok(match &receipt.receipt { + ReceiptEnum::Action(action_receipt) => { + let mut required_gas = safe_add_gas( + total_prepaid_exec_fees( + &apply_state.config.fees, + &action_receipt.actions, + &receipt.receiver_id, + apply_state.current_protocol_version, + )?, + total_prepaid_gas(&action_receipt.actions)?, + )?; + required_gas = safe_add_gas( + required_gas, + apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee(), + )?; + + required_gas + } + ReceiptEnum::Data(_) => 0, + }) +} + +/// Validate access key which was used for signing DelegateAction: +/// +/// - Checks whether the access key is present fo given public_key and sender_id. +/// - Validates nonce and updates it if it's ok. +/// - Validates access key permissions. +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +fn validate_delegate_action_key( + state_update: &mut TrieUpdate, + apply_state: &ApplyState, + delegate_action: &DelegateAction, + result: &mut ActionResult, +) -> Result<(), RuntimeError> { + // 'delegate_action.sender_id' account existence must be checked by a caller + let mut access_key = match get_access_key( + state_update, + &delegate_action.sender_id, + &delegate_action.public_key, + )? { + Some(access_key) => access_key, + None => { + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: delegate_action.sender_id.clone(), + public_key: delegate_action.public_key.clone(), + }, + ) + .into()); + return Ok(()); + } + }; + + if delegate_action.nonce <= access_key.nonce { + result.result = Err(ActionErrorKind::DelegateActionInvalidNonce { + delegate_nonce: delegate_action.nonce, + ak_nonce: access_key.nonce, + } + .into()); + return Ok(()); + } + + let upper_bound = apply_state.block_height + * near_primitives::account::AccessKey::ACCESS_KEY_NONCE_RANGE_MULTIPLIER; + if delegate_action.nonce >= upper_bound { + result.result = Err(ActionErrorKind::DelegateActionNonceTooLarge { + delegate_nonce: delegate_action.nonce, + upper_bound, + } + .into()); + return Ok(()); + } + + access_key.nonce = delegate_action.nonce; + + let actions = delegate_action.get_actions(); + + // The restriction of "function call" access keys: + // the transaction must contain the only `FunctionCall` if "function call" access key is used + if let AccessKeyPermission::FunctionCall(ref function_call_permission) = access_key.permission { + if actions.len() != 1 { + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()); + return Ok(()); + } + if let Some(Action::FunctionCall(ref function_call)) = actions.get(0) { + if function_call.deposit > 0 { + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::DepositWithFunctionCall, + ) + .into()); + } + if delegate_action.receiver_id.as_ref() != function_call_permission.receiver_id { + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::ReceiverMismatch { + tx_receiver: delegate_action.receiver_id.clone(), + ak_receiver: function_call_permission.receiver_id.clone(), + }, + ) + .into()); + return Ok(()); + } + if !function_call_permission.method_names.is_empty() + && function_call_permission + .method_names + .iter() + .all(|method_name| &function_call.method_name != method_name) + { + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::MethodNameMismatch { + method_name: function_call.method_name.clone(), + }, + ) + .into()); + return Ok(()); + } + } else { + // There should Action::FunctionCall when "function call" permission is used + result.result = Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()); + return Ok(()); + } + }; + + set_access_key( + state_update, + delegate_action.sender_id.clone(), + delegate_action.public_key.clone(), + &access_key, + ); + + Ok(()) +} + pub(crate) fn check_actor_permissions( action: &Action, account: &Option, @@ -657,6 +883,8 @@ pub(crate) fn check_actor_permissions( } } Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Action::Delegate(_) => (), }; Ok(()) } @@ -731,12 +959,22 @@ pub(crate) fn check_account_existence( .into()); } } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Action::Delegate(_) => { + if account.is_none() { + return Err(ActionErrorKind::AccountDoesNotExist { + account_id: account_id.clone(), + } + .into()); + } + } }; Ok(()) } #[cfg(test)] mod tests { + use near_primitives::hash::hash; use near_primitives::trie_key::TrieKey; use near_store::test_utils::create_tries; @@ -744,6 +982,21 @@ mod tests { use super::*; use crate::near_primitives::shard_layout::ShardUId; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::account::FunctionCallPermission; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::errors::InvalidAccessKeyError; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::runtime::migration_data::MigrationFlags; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::transaction::{CreateAccountAction, NonDelegateAction}; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::types::{EpochId, StateChangeCause}; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_store::set_account; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use std::sync::Arc; + fn test_action_create_account( account_id: AccountId, predecessor_id: AccountId, @@ -921,4 +1174,602 @@ mod tests { }) ); } + + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn create_delegate_action_receipt() -> (ActionReceipt, SignedDelegateAction) { + let signed_delegate_action = SignedDelegateAction { + delegate_action: DelegateAction { + sender_id: "bob.test.near".parse().unwrap(), + receiver_id: "token.test.near".parse().unwrap(), + actions: vec![ + NonDelegateAction( + Action::FunctionCall( + FunctionCallAction { + method_name: "ft_transfer".parse().unwrap(), + args: vec![123, 34, 114, 101, 99, 101, 105, 118, 101, 114, 95, 105, 100, 34, 58, 34, 106, 97, 110, 101, 46, 116, 101, 115, 116, 46, 110, 101, 97, 114, 34, 44, 34, 97, 109, 111, 117, 110, 116, 34, 58, 34, 52, 34, 125], + gas: 30000000000000, + deposit: 1, + } + ) + ) + ], + nonce: 19000001, + max_block_height: 57, + public_key: "ed25519:HaYUbyeiNRnyHtQceRgT3gyMBigZFEW9EYYU1KTHtdR1".parse::().unwrap(), + }, + signature: "ed25519:2b1NHmrj7LVgA5H9aDtQmd6JgZqy4nPAYHtNQc88PiEY3xMjpkKMDN1wVWZaXMGx9tjWbXzp4jXSCyTPqUfPdRUB".parse().unwrap() + }; + + let action_receipt = ActionReceipt { + signer_id: "alice.test.near".parse().unwrap(), + signer_public_key: PublicKey::empty(near_crypto::KeyType::ED25519), + gas_price: 1, + output_data_receivers: Vec::new(), + input_data_ids: Vec::new(), + actions: vec![Action::Delegate(signed_delegate_action.clone())], + }; + + (action_receipt, signed_delegate_action) + } + + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn create_apply_state(block_height: BlockHeight) -> ApplyState { + ApplyState { + block_height, + prev_block_hash: CryptoHash::default(), + block_hash: CryptoHash::default(), + epoch_id: EpochId::default(), + epoch_height: 3, + gas_price: 2, + block_timestamp: 1, + gas_limit: None, + random_seed: CryptoHash::default(), + current_protocol_version: 1, + config: Arc::new(RuntimeConfig::test()), + cache: None, + is_new_chunk: false, + migration_data: Arc::default(), + migration_flags: MigrationFlags::default(), + } + } + + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn setup_account( + account_id: &AccountId, + public_key: &PublicKey, + access_key: &AccessKey, + ) -> TrieUpdate { + 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); + set_account(&mut state_update, account_id.clone(), &account); + set_access_key(&mut state_update, account_id.clone(), public_key.clone(), access_key); + + state_update.commit(StateChangeCause::InitialState); + let trie_changes = state_update.finalize().unwrap().0; + let mut store_update = tries.store_update(); + let root = tries.apply_all(&trie_changes, ShardUId::single_shard(), &mut store_update); + store_update.commit().unwrap(); + + tries.new_trie_update(ShardUId::single_shard(), root) + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action() { + let mut result = ActionResult::default(); + let (action_receipt, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + apply_delegate_action( + &mut state_update, + &apply_state, + &action_receipt, + &sender_id, + &signed_delegate_action, + &mut result, + ) + .expect("Expect ok"); + + assert!(result.result.is_ok(), "Result error: {:?}", result.result.err()); + assert_eq!( + result.new_receipts, + vec![Receipt { + predecessor_id: sender_id.clone(), + receiver_id: signed_delegate_action.delegate_action.receiver_id.clone(), + receipt_id: CryptoHash::default(), + receipt: ReceiptEnum::Action(ActionReceipt { + signer_id: action_receipt.signer_id.clone(), + signer_public_key: action_receipt.signer_public_key.clone(), + gas_price: action_receipt.gas_price, + output_data_receivers: Vec::new(), + input_data_ids: Vec::new(), + actions: signed_delegate_action.delegate_action.get_actions(), + }) + }] + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_signature_verification() { + let mut result = ActionResult::default(); + let (action_receipt, mut signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + // Corrupt receiver_id. Signature verifycation must fail. + signed_delegate_action.delegate_action.receiver_id = "www.test.near".parse().unwrap(); + + apply_delegate_action( + &mut state_update, + &apply_state, + &action_receipt, + &sender_id, + &signed_delegate_action, + &mut result, + ) + .expect("Expect ok"); + + assert_eq!(result.result, Err(ActionErrorKind::DelegateActionInvalidSignature.into())); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_max_height() { + let mut result = ActionResult::default(); + let (action_receipt, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + // Setup current block as higher than max_block_height. Must fail. + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height + 1); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + apply_delegate_action( + &mut state_update, + &apply_state, + &action_receipt, + &sender_id, + &signed_delegate_action, + &mut result, + ) + .expect("Expect ok"); + + assert_eq!(result.result, Err(ActionErrorKind::DelegateActionExpired.into())); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_validate_sender_account() { + let mut result = ActionResult::default(); + let (action_receipt, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + // Use a different sender_id. Must fail. + apply_delegate_action( + &mut state_update, + &apply_state, + &action_receipt, + &"www.test.near".parse().unwrap(), + &signed_delegate_action, + &mut result, + ) + .expect("Expect ok"); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionSenderDoesNotMatchTxReceiver { + sender_id: sender_id.clone(), + receiver_id: "www.test.near".parse().unwrap(), + } + .into()) + ); + + // Sender account doesn't exist. Must fail. + assert_eq!( + check_account_existence( + &Action::Delegate(signed_delegate_action), + &mut None, + &sender_id, + 1, + false, + false + ), + Err(ActionErrorKind::AccountDoesNotExist { account_id: sender_id.clone() }.into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_validate_delegate_action_key_update_nonce() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + // Everything is ok + let mut result = ActionResult::default(); + validate_delegate_action_key( + &mut state_update, + &apply_state, + &signed_delegate_action.delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert!(result.result.is_ok(), "Result error: {:?}", result.result); + + // Must fail, Nonce had been updated by previous step. + result = ActionResult::default(); + validate_delegate_action_key( + &mut state_update, + &apply_state, + &signed_delegate_action.delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionInvalidNonce { + delegate_nonce: signed_delegate_action.delegate_action.nonce, + ak_nonce: signed_delegate_action.delegate_action.nonce, + } + .into()) + ); + + // Increment nonce. Must pass. + result = ActionResult::default(); + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.nonce += 1; + validate_delegate_action_key( + &mut state_update, + &apply_state, + &delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert!(result.result.is_ok(), "Result error: {:?}", result.result); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_doesnt_exist() { + let mut result = ActionResult::default(); + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account( + &sender_id, + &PublicKey::empty(near_crypto::KeyType::ED25519), + &access_key, + ); + + validate_delegate_action_key( + &mut state_update, + &apply_state, + &signed_delegate_action.delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: sender_id.clone(), + public_key: sender_pub_key.clone(), + }, + ) + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_incorrect_nonce() { + let mut result = ActionResult::default(); + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { + nonce: signed_delegate_action.delegate_action.nonce, + permission: AccessKeyPermission::FullAccess, + }; + + let apply_state = + create_apply_state(signed_delegate_action.delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + validate_delegate_action_key( + &mut state_update, + &apply_state, + &signed_delegate_action.delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionInvalidNonce { + delegate_nonce: signed_delegate_action.delegate_action.nonce, + ak_nonce: signed_delegate_action.delegate_action.nonce, + } + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_nonce_too_large() { + let mut result = ActionResult::default(); + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let sender_id = signed_delegate_action.delegate_action.sender_id.clone(); + let sender_pub_key = signed_delegate_action.delegate_action.public_key.clone(); + let access_key = AccessKey { nonce: 19000000, permission: AccessKeyPermission::FullAccess }; + + let apply_state = create_apply_state(1); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + validate_delegate_action_key( + &mut state_update, + &apply_state, + &signed_delegate_action.delegate_action, + &mut result, + ) + .expect("Expect ok"); + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionNonceTooLarge { + delegate_nonce: signed_delegate_action.delegate_action.nonce, + upper_bound: 1000000, + } + .into()) + ); + } + + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions( + access_key: &AccessKey, + delegate_action: &DelegateAction, + ) -> ActionResult { + let mut result = ActionResult::default(); + let sender_id = delegate_action.sender_id.clone(); + let sender_pub_key = delegate_action.public_key.clone(); + + let apply_state = create_apply_state(delegate_action.max_block_height); + let mut state_update = setup_account(&sender_id, &sender_pub_key, &access_key); + + validate_delegate_action_key( + &mut state_update, + &apply_state, + &delegate_action, + &mut result, + ) + .expect("Expect ok"); + + result + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_fncall() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: signed_delegate_action.delegate_action.receiver_id.to_string(), + method_names: vec!["test_method".parse().unwrap()], + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = + vec![NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 0, + gas: 300, + method_name: "test_method".parse().unwrap(), + }))]; + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + assert!(result.result.is_ok(), "Result error {:?}", result.result); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_incorrect_action() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: signed_delegate_action.delegate_action.receiver_id.to_string(), + method_names: vec!["test_method".parse().unwrap()], + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = + vec![NonDelegateAction(Action::CreateAccount(CreateAccountAction {}))]; + + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_actions_number() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: signed_delegate_action.delegate_action.receiver_id.to_string(), + method_names: vec!["test_method".parse().unwrap()], + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = vec![ + NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 0, + gas: 300, + method_name: "test_method".parse().unwrap(), + })), + NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 0, + gas: 300, + method_name: "test_method".parse().unwrap(), + })), + ]; + + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_fncall_deposit() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: signed_delegate_action.delegate_action.receiver_id.to_string(), + method_names: Vec::new(), + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = + vec![NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 1, + gas: 300, + method_name: "test_method".parse().unwrap(), + }))]; + + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::DepositWithFunctionCall, + ) + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_receiver_id() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: "another.near".parse().unwrap(), + method_names: Vec::new(), + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = + vec![NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 0, + gas: 300, + method_name: "test_method".parse().unwrap(), + }))]; + + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::ReceiverMismatch { + tx_receiver: delegate_action.receiver_id.clone(), + ak_receiver: "another.near".parse().unwrap(), + }, + ) + .into()) + ); + } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_key_permissions_method() { + let (_, signed_delegate_action) = create_delegate_action_receipt(); + let access_key = AccessKey { + nonce: 19000000, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: None, + receiver_id: signed_delegate_action.delegate_action.receiver_id.to_string(), + method_names: vec!["another_method".parse().unwrap()], + }), + }; + + let mut delegate_action = signed_delegate_action.delegate_action.clone(); + delegate_action.actions = + vec![NonDelegateAction(Action::FunctionCall(FunctionCallAction { + args: Vec::new(), + deposit: 0, + gas: 300, + method_name: "test_method".parse().unwrap(), + }))]; + + let result = test_delegate_action_key_permissions(&access_key, &delegate_action); + + assert_eq!( + result.result, + Err(ActionErrorKind::DelegateActionAccessKeyError( + InvalidAccessKeyError::MethodNameMismatch { + method_name: "test_method".parse().unwrap(), + }, + ) + .into()) + ); + } } diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index ca6feb59918..34cef10e967 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -2,7 +2,7 @@ use crate::safe_add_balance_apply; use crate::config::{ safe_add_balance, safe_add_gas, safe_gas_to_balance, total_deposit, total_prepaid_exec_fees, - total_prepaid_gas, + total_prepaid_gas, total_prepaid_send_fees, }; use crate::{ApplyStats, DelayedReceiptIndices, ValidatorAccountsUpdate}; use near_primitives::errors::{ @@ -55,6 +55,14 @@ fn receipt_cost( )?, )?; total_gas = safe_add_gas(total_gas, total_prepaid_gas(&action_receipt.actions)?)?; + total_gas = safe_add_gas( + total_gas, + total_prepaid_send_fees( + transaction_costs, + &action_receipt.actions, + current_protocol_version, + )?, + )?; let total_gas_cost = safe_gas_to_balance(action_receipt.gas_price, total_gas)?; total_cost = safe_add_balance(total_cost, total_gas_cost)?; } diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index a6e516e51c3..0dcfa933cc8 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -125,12 +125,75 @@ pub fn total_send_fees( DeleteAccount(_) => { config.fee(ActionCosts::delete_account).send_fee(sender_is_receiver) } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Delegate(signed_delegate_action) => { + let delegate_cost = config.fee(ActionCosts::delegate).send_fee(sender_is_receiver); + let delegate_action = &signed_delegate_action.delegate_action; + + delegate_cost + + total_send_fees( + config, + sender_is_receiver, + &delegate_action.get_actions(), + &delegate_action.receiver_id, + current_protocol_version, + )? + } + }; + result = safe_add_gas(result, delta)?; + } + Ok(result) +} + +/// Total sum of gas that needs to be burnt to send the inner actions of DelegateAction +/// +/// This is only relevant for DelegateAction, where the send fees of the inner actions +/// need to be prepaid. All other actions burn send fees directly, so calling this function +/// with other actions will return 0. +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +pub fn total_prepaid_send_fees( + config: &RuntimeFeesConfig, + actions: &[Action], + current_protocol_version: ProtocolVersion, +) -> Result { + let mut result = 0; + + for action in actions { + use Action::*; + let delta = match action { + Delegate(signed_delegate_action) => { + let delegate_action = &signed_delegate_action.delegate_action; + let sender_is_receiver = delegate_action.sender_id == delegate_action.receiver_id; + + total_send_fees( + config, + sender_is_receiver, + &delegate_action.get_actions(), + &delegate_action.receiver_id, + current_protocol_version, + )? + } + _ => 0, }; result = safe_add_gas(result, delta)?; } Ok(result) } +/// Total sum of gas that needs to be burnt to send the inner actions of DelegateAction +/// +/// This is only relevant for DelegateAction, where the send fees of the inner actions +/// need to be prepaid. All other actions burn send fees directly, so calling this function +/// with other actions will return 0. +#[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] +pub fn total_prepaid_send_fees( + _config: &RuntimeFeesConfig, + _actions: &[Action], + _current_protocol_version: ProtocolVersion, +) -> Result { + Ok(0) +} + pub fn exec_fee( config: &RuntimeFeesConfig, action: &Action, @@ -176,6 +239,8 @@ pub fn exec_fee( }, DeleteKey(_) => config.fee(ActionCosts::delete_key).exec_fee(), DeleteAccount(_) => config.fee(ActionCosts::delete_account).exec_fee(), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Delegate(_) => config.fee(ActionCosts::delegate).exec_fee(), } } @@ -199,7 +264,10 @@ pub fn tx_cost( current_protocol_version, )?, )?; - let prepaid_gas = total_prepaid_gas(&transaction.actions)?; + let prepaid_gas = safe_add_gas( + total_prepaid_gas(&transaction.actions)?, + total_prepaid_send_fees(config, &transaction.actions, current_protocol_version)?, + )?; // If signer is equals to receiver the receipt will be processed at the same block as this // transaction. Otherwise it will processed in the next block and the gas might be inflated. let initial_receipt_hop = if transaction.signer_id == transaction.receiver_id { 0 } else { 1 }; @@ -246,7 +314,36 @@ pub fn total_prepaid_exec_fees( ) -> Result { let mut result = 0; for action in actions { - let delta = exec_fee(config, action, receiver_id, current_protocol_version); + #[cfg_attr(not(feature = "protocol_feature_nep366_delegate_action"), allow(unused_mut))] + let mut delta; + // In case of Action::Delegate it's needed to add Gas which is required for the inner actions. + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + if let Action::Delegate(signed_delegate_action) = action { + let actions = signed_delegate_action.delegate_action.get_actions(); + delta = total_prepaid_exec_fees( + config, + &actions, + &signed_delegate_action.delegate_action.receiver_id, + current_protocol_version, + )?; + delta = safe_add_gas( + delta, + exec_fee( + config, + action, + &signed_delegate_action.delegate_action.receiver_id, + current_protocol_version, + ), + )?; + delta = safe_add_gas(delta, config.fee(ActionCosts::new_action_receipt).exec_fee())?; + } else { + delta = exec_fee(config, action, receiver_id, current_protocol_version); + } + #[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] + { + delta = exec_fee(config, action, receiver_id, current_protocol_version); + } + result = safe_add_gas(result, delta)?; } Ok(result) @@ -255,14 +352,46 @@ pub fn total_prepaid_exec_fees( pub fn total_deposit(actions: &[Action]) -> Result { let mut total_balance: Balance = 0; for action in actions { - total_balance = safe_add_balance(total_balance, action.get_deposit_balance())?; + let action_balance; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + if let Action::Delegate(signed_delegate_action) = action { + // Note, here Relayer pays the deposit but if actions fail, the deposit is + // refunded to Sender of DelegateAction + let actions = signed_delegate_action.delegate_action.get_actions(); + action_balance = total_deposit(&actions)?; + } else { + action_balance = action.get_deposit_balance(); + } + #[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] + { + action_balance = action.get_deposit_balance(); + } + + total_balance = safe_add_balance(total_balance, action_balance)?; } Ok(total_balance) } /// Get the total sum of prepaid gas for given actions. pub fn total_prepaid_gas(actions: &[Action]) -> Result { - actions.iter().try_fold(0, |acc, action| safe_add_gas(acc, action.get_prepaid_gas())) + let mut total_gas: Gas = 0; + for action in actions { + let action_gas; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + if let Action::Delegate(signed_delegate_action) = action { + let actions = signed_delegate_action.delegate_action.get_actions(); + action_gas = total_prepaid_gas(&actions)?; + } else { + action_gas = action.get_prepaid_gas(); + } + #[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] + { + action_gas = action.get_prepaid_gas(); + } + + total_gas = safe_add_gas(total_gas, action_gas)?; + } + Ok(total_gas) } #[cfg(test)] diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 65d98141074..5b21809c55a 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::sync::Arc; +use config::total_prepaid_send_fees; use near_primitives::sandbox::state_patch::SandboxStatePatch; use tracing::debug; @@ -443,6 +444,17 @@ impl Runtime { apply_state.current_protocol_version, )?; } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Action::Delegate(signed_delegate_action) => { + apply_delegate_action( + state_update, + apply_state, + action_receipt, + account_id, + signed_delegate_action, + &mut result, + )?; + } }; Ok(result) } @@ -747,7 +759,14 @@ impl Runtime { transaction_costs: &RuntimeFeesConfig, ) -> Result { let total_deposit = total_deposit(&action_receipt.actions)?; - let prepaid_gas = total_prepaid_gas(&action_receipt.actions)?; + let prepaid_gas = safe_add_gas( + total_prepaid_gas(&action_receipt.actions)?, + total_prepaid_send_fees( + transaction_costs, + &action_receipt.actions, + current_protocol_version, + )?, + )?; let prepaid_exec_gas = safe_add_gas( total_prepaid_exec_fees( transaction_costs, @@ -792,6 +811,7 @@ impl Runtime { )?, )?; } + if deposit_refund > 0 { result .new_receipts diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 309bfb2fee6..e7df43e7a3a 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -25,6 +25,9 @@ use near_primitives::checked_feature; use near_primitives::runtime::config::RuntimeConfig; use near_primitives::types::BlockHeight; +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +use near_primitives::transaction::SignedDelegateAction; + /// Validates the transaction without using the state. It allows any node to validate a /// transaction before forwarding it to the node that tracks the `signer_id` account. pub fn validate_transaction( @@ -274,6 +277,7 @@ fn validate_data_receipt( /// /// - Checks limits if applicable. /// - Checks that the total number of actions doesn't exceed the limit. +/// - Checks that there not other action if Action::Delegate is present. /// - Validates each individual action. /// - Checks that the total prepaid gas doesn't exceed the limit. pub(crate) fn validate_actions( @@ -287,12 +291,22 @@ pub(crate) fn validate_actions( }); } + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + let mut found_delegate_action = false; let mut iter = actions.iter().peekable(); while let Some(action) = iter.next() { if let Action::DeleteAccount(_) = action { if iter.peek().is_some() { return Err(ActionsValidationError::DeleteActionMustBeFinal); } + } else { + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + if let Action::Delegate(_) = action { + if found_delegate_action { + return Err(ActionsValidationError::DelegateActionMustBeOnlyOne); + } + found_delegate_action = true; + } } validate_action(limit_config, action)?; } @@ -323,9 +337,21 @@ pub fn validate_action( Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), Action::DeleteAccount(_) => Ok(()), + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + Action::Delegate(a) => validate_delegate_action(limit_config, a), } } +#[cfg(feature = "protocol_feature_nep366_delegate_action")] +fn validate_delegate_action( + limit_config: &VMLimitConfig, + signed_delegate_action: &SignedDelegateAction, +) -> Result<(), ActionsValidationError> { + let actions = signed_delegate_action.delegate_action.get_actions(); + validate_actions(limit_config, &actions)?; + Ok(()) +} + /// Validates `DeployContractAction`. Checks that the given contract size doesn't exceed the limit. fn validate_deploy_contract_action( limit_config: &VMLimitConfig, @@ -468,6 +494,11 @@ mod tests { use super::*; use crate::near_primitives::shard_layout::ShardUId; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_crypto::Signature; + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + use near_primitives::transaction::{DelegateAction, NonDelegateAction}; + /// Initial balance used in tests. const TESTING_INIT_BALANCE: Balance = 1_000_000_000 * NEAR_BASE; @@ -1504,4 +1535,47 @@ mod tests { ) .expect("valid action"); } + + #[test] + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + fn test_delegate_action_must_be_only_one() { + let signed_delegate_action = SignedDelegateAction { + delegate_action: DelegateAction { + sender_id: "bob.test.near".parse().unwrap(), + receiver_id: "token.test.near".parse().unwrap(), + actions: vec![NonDelegateAction(Action::CreateAccount(CreateAccountAction {}))], + nonce: 19000001, + max_block_height: 57, + public_key: PublicKey::empty(KeyType::ED25519), + }, + signature: Signature::default(), + }; + assert_eq!( + validate_actions( + &VMLimitConfig::test(), + &[ + Action::Delegate(signed_delegate_action.clone()), + Action::Delegate(signed_delegate_action.clone()), + ] + ), + Err(ActionsValidationError::DelegateActionMustBeOnlyOne), + ); + assert_eq!( + validate_actions( + &&VMLimitConfig::test(), + &[Action::Delegate(signed_delegate_action.clone()),] + ), + Ok(()), + ); + assert_eq!( + validate_actions( + &VMLimitConfig::test(), + &[ + Action::CreateAccount(CreateAccountAction {}), + Action::Delegate(signed_delegate_action.clone()), + ] + ), + Ok(()), + ); + } }