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(()), + ); + } }