diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index b2008c7b301..4817252b1a3 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -42,7 +42,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, @"2iGtRFjF6BcqPF6tDcfLLojRaNax2PKDLxRqRc3RxRn7"); } @@ -72,7 +72,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, @"7BkghFM7ZA8piYHAWYu4vTY6vE1pkTwy14bqQnS138qE"); } 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/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index 098bb5893e0..5d51fbdfe64 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -419,6 +419,7 @@ impl From for Vec { ); operations.push(deploy_contract_operation); } + near_primitives::transaction::Action::Delegate(_) => todo!(), } } operations diff --git a/core/primitives-core/src/parameter.rs b/core/primitives-core/src/parameter.rs index 4c3096cab25..1a9aaa44636 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 { diff --git a/core/primitives-core/src/runtime/fees.rs b/core/primitives-core/src/runtime/fees.rs index f02b4fed20d..e95d991278b 100644 --- a/core/primitives-core/src/runtime/fees.rs +++ b/core/primitives-core/src/runtime/fees.rs @@ -113,6 +113,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. @@ -219,6 +222,11 @@ impl RuntimeFeesConfig { send_not_sir: 147489000000, execution: 147489000000, }, + delegate_cost: Fee { + send_sir: 2319861500000, + send_not_sir: 2319861500000, + execution: 2319861500000, + }, }, storage_usage_config: StorageUsageConfig { // See Account in core/primitives/src/account.rs for the data structure. @@ -253,7 +261,8 @@ impl RuntimeFeesConfig { function_call_cost_per_byte: free.clone(), }, delete_key_cost: free.clone(), - delete_account_cost: free, + delete_account_cost: free.clone(), + delegate_cost: free, }, storage_usage_config: StorageUsageConfig { num_bytes_account: 0, diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index ae758168f57..2639902f447 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -48,12 +48,14 @@ protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_ed25519_verify = [ "near-primitives-core/protocol_feature_ed25519_verify" ] +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.txt b/core/primitives/res/runtime_configs/parameters.txt index ceeb3ac3333..f3de1f5c943 100644 --- a/core/primitives/res/runtime_configs/parameters.txt +++ b/core/primitives/res/runtime_configs/parameters.txt @@ -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/res/runtime_configs/parameters_testnet.txt b/core/primitives/res/runtime_configs/parameters_testnet.txt index d8223613ecf..13aaaf90d50 100644 --- a/core/primitives/res/runtime_configs/parameters_testnet.txt +++ b/core/primitives/res/runtime_configs/parameters_testnet.txt @@ -62,6 +62,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/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index fb8c3ed4d18..f9b05a2cec6 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -125,6 +125,7 @@ impl ParameterTable { }, "delete_key_cost": self.fee_json(FeeParameter::ActionDeleteKey), "delete_account_cost": self.fee_json(FeeParameter::ActionDeleteAccount), + "delegate_cost": self.fee_json(FeeParameter::ActionDelegate), }, "storage_usage_config": { "num_bytes_account": self.get(Parameter::StorageNumBytesAccount), 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..8b659f0c87d 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..d6e5a4f0633 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..1d73753cc92 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..b74495c8892 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..1a3ab756d54 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..843de731144 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..685e6578c1f 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..c7c45874652 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..8b659f0c87d 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..d6e5a4f0633 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..1d73753cc92 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..b74495c8892 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..1a3ab756d54 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..843de731144 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..685e6578c1f 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { 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..c7c45874652 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 @@ -84,6 +84,11 @@ expression: store.get_config(*version) "send_sir": 147489000000, "send_not_sir": 147489000000, "execution": 147489000000 + }, + "delegate_cost": { + "send_sir": 2319861500000, + "send_not_sir": 2319861500000, + "execution": 2319861500000 } }, "storage_usage_config": { diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 46f82880ef4..13503d87b82 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,7 @@ pub enum Action { AddKey(AddKeyAction), DeleteKey(DeleteKeyAction), DeleteAccount(DeleteAccountAction), + Delegate(SignedDelegateAction), } impl Action { @@ -210,6 +216,94 @@ 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) + } +} + +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 { @@ -549,4 +643,67 @@ mod tests { outcome.to_hashes() ); } + + 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 + ); + + } + + #[test] + #[cfg(not(feature = "protocol_feature_nep366_delegate_action"))] + fn test_delegate_action_deserialization() { + let delegate_action = + create_delegate_action(vec![Action::CreateAccount(CreateAccountAction {})]); + let serialized_delegate_action = delegate_action.try_to_vec().expect("Expect ok"); + + // DelegateAction isn't supported + assert_eq!( + Action::try_from_slice(&serialized_delegate_action).map_err(|e| e.kind()), + Err(ErrorKind::InvalidInput) + ); + } } diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index 345c8cc349c..fd39a6095c9 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -151,6 +151,8 @@ pub enum ProtocolFeature { RejectBlocksWithOutdatedProtocolVersions, #[cfg(feature = "shardnet")] ShardnetShardLayoutUpgrade, + #[cfg(feature = "protocol_feature_nep366_delegate_action")] + DelegateAction, } /// Both, outgoing and incoming tcp connections to peers, will be rejected if `peer's` @@ -166,7 +168,7 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 57; /// 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 if cfg!(feature = "shardnet") { 102 } else { @@ -254,6 +256,8 @@ impl ProtocolFeature { } #[cfg(feature = "shardnet")] ProtocolFeature::ShardnetShardLayoutUpgrade => 102, + #[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 a2a2fc4924b..9ff725a399a 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -36,10 +36,10 @@ use crate::sharding::{ ShardChunkHeaderV3, }; use crate::transaction::{ - Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, - DeployContractAction, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithIdAndProof, - ExecutionStatus, FunctionCallAction, PartialExecutionOutcome, PartialExecutionStatus, - SignedTransaction, StakeAction, TransferAction, + Action, AddKeyAction, CreateAccountAction, DelegateAction, DeleteAccountAction, + DeleteKeyAction, DeployContractAction, ExecutionMetadata, ExecutionOutcome, + ExecutionOutcomeWithIdAndProof, ExecutionStatus, FunctionCallAction, PartialExecutionOutcome, + PartialExecutionStatus, SignedDelegateAction, SignedTransaction, StakeAction, TransferAction, }; use crate::types::{ AccountId, AccountWithPublicKey, Balance, BlockHeight, CompiledContractCache, EpochHeight, @@ -1050,6 +1050,10 @@ pub enum ActionView { DeleteAccount { beneficiary_id: AccountId, }, + Delegate { + delegate_action: DelegateAction, + signature: Signature, + }, } impl From for ActionView { @@ -1078,6 +1082,10 @@ impl From for ActionView { Action::DeleteAccount(action) => { ActionView::DeleteAccount { beneficiary_id: action.beneficiary_id } } + Action::Delegate(action) => ActionView::Delegate { + delegate_action: action.delegate_action, + signature: action.signature, + }, } } } @@ -1107,6 +1115,12 @@ impl TryFrom for Action { ActionView::DeleteAccount { beneficiary_id } => { Action::DeleteAccount(DeleteAccountAction { beneficiary_id }) } + ActionView::Delegate { delegate_action, signature } => { + Action::Delegate(SignedDelegateAction { + delegate_action: delegate_action, + signature, + }) + } }) } } diff --git a/nearcore/Cargo.toml b/nearcore/Cargo.toml index 47f9d168714..d3937de67ed 100644 --- a/nearcore/Cargo.toml +++ b/nearcore/Cargo.toml @@ -109,6 +109,11 @@ protocol_feature_fix_contract_loading_cost = [ "near-vm-runner/protocol_feature_fix_contract_loading_cost", ] 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", +] + nightly = [ "nightly_protocol", @@ -118,6 +123,7 @@ nightly = [ "near-store/nightly", "protocol_feature_fix_staking_threshold", "protocol_feature_fix_contract_loading_cost", + "protocol_feature_nep366_delegate_action", ] nightly_protocol = [ "near-primitives/nightly_protocol", diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 47a9aeecb57..d60134d652a 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -62,6 +62,7 @@ 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"] cold_store = ["nearcore/cold_store", "near-store/cold_store", "near-cold-store-tool/cold_store"] +protocol_feature_nep366_delegate_action = ["nearcore/protocol_feature_nep366_delegate_action"] nightly = [ "nightly_protocol", diff --git a/runtime/runtime-params-estimator/src/cost.rs b/runtime/runtime-params-estimator/src/cost.rs index a8ad2428e7a..f2e10e4a16e 100644 --- a/runtime/runtime-params-estimator/src/cost.rs +++ b/runtime/runtime-params-estimator/src/cost.rs @@ -154,6 +154,9 @@ pub enum Cost { /// Subtract the base cost of creating a sir-receipt. /// TODO(jakmeier): Consider different account states. ActionDeleteAccount, + /// 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 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 f6b65d0241d..7d5fed5e6fe 100644 --- a/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs +++ b/runtime/runtime-params-estimator/src/costs_to_runtime_config.rs @@ -78,6 +78,7 @@ fn runtime_fees_config(cost_table: &CostTable) -> anyhow::Result 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.transaction_costs, + &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 +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.transaction_costs, + &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.transaction_costs.action_receipt_creation_config.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. +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, @@ -656,7 +871,10 @@ pub(crate) fn check_actor_permissions( .into()); } } - Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), + Action::CreateAccount(_) + | Action::FunctionCall(_) + | Action::Transfer(_) + | Action::Delegate(_) => (), }; Ok(()) } @@ -723,7 +941,8 @@ pub(crate) fn check_account_existence( | Action::Stake(_) | Action::AddKey(_) | Action::DeleteKey(_) - | Action::DeleteAccount(_) => { + | Action::DeleteAccount(_) + | Action::Delegate(_) => { if account.is_none() { return Err(ActionErrorKind::AccountDoesNotExist { account_id: account_id.clone(), @@ -737,8 +956,15 @@ pub(crate) fn check_account_existence( #[cfg(test)] mod tests { + use std::sync::Arc; + + use near_primitives::account::FunctionCallPermission; use near_primitives::hash::hash; + use near_primitives::runtime::migration_data::MigrationFlags; + use near_primitives::transaction::{CreateAccountAction, NonDelegateAction}; use near_primitives::trie_key::TrieKey; + use near_primitives::types::{EpochId, StateChangeCause}; + use near_store::set_account; use near_store::test_utils::create_tries; use super::*; @@ -921,4 +1147,584 @@ mod tests { }) ); } + + 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) + } + + 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(), + } + } + + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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()) + ); + } + + 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] + 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] + 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] + 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] + 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] + 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] + 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 5c6cbfcf8e7..fb6b89119a7 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::{ @@ -54,6 +54,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 ee133a8788b..6c1c2dddcd4 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -120,6 +120,53 @@ pub fn total_send_fees( }, DeleteKey(_) => cfg.delete_key_cost.send_fee(sender_is_receiver), DeleteAccount(_) => cfg.delete_account_cost.send_fee(sender_is_receiver), + Delegate(signed_delegate_action) => { + let delegate_cost = cfg.delegate_cost.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. +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)?; } @@ -170,6 +217,7 @@ pub fn exec_fee( }, DeleteKey(_) => cfg.delete_key_cost.exec_fee(), DeleteAccount(_) => cfg.delete_account_cost.exec_fee(), + Delegate(_) => cfg.delegate_cost.exec_fee(), } } @@ -192,7 +240,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 }; @@ -239,7 +290,29 @@ 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); + let mut delta; + // In case of Action::Delegate it's needed to add Gas which is required for the inner actions. + 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.action_receipt_creation_config.exec_fee())?; + } else { + delta = exec_fee(config, action, receiver_id, current_protocol_version); + } result = safe_add_gas(result, delta)?; } Ok(result) @@ -248,14 +321,34 @@ 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; + 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(); + } + 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; + 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(); + } + 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 2eec9cbe1a7..61ed0117cef 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,16 @@ impl Runtime { apply_state.current_protocol_version, )?; } + Action::Delegate(signed_delegate_action) => { + apply_delegate_action( + state_update, + apply_state, + action_receipt, + account_id, + signed_delegate_action, + &mut result, + )?; + } }; Ok(result) } @@ -748,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, @@ -793,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 9fa6369b21c..e546fcc8e9e 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -1,5 +1,6 @@ use near_crypto::key_conversion::is_valid_staking_key; use near_primitives::runtime::get_insufficient_storage_stake; +use near_primitives::transaction::SignedDelegateAction; use near_primitives::{ account::AccessKeyPermission, config::VMLimitConfig, @@ -280,6 +281,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( @@ -293,12 +295,18 @@ pub(crate) fn validate_actions( }); } + 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 if let Action::Delegate(_) = action { + if found_delegate_action { + return Err(ActionsValidationError::DelegateActionMustBeOnlyOne); + } + found_delegate_action = true; } validate_action(limit_config, action)?; } @@ -329,9 +337,19 @@ pub fn validate_action( Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), Action::DeleteAccount(_) => Ok(()), + Action::Delegate(a) => validate_delegate_action(limit_config, a), } } +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, @@ -459,12 +477,12 @@ fn test_truncate_string() { mod tests { use std::sync::Arc; - use near_crypto::{InMemorySigner, KeyType, PublicKey, Signer}; + use near_crypto::{InMemorySigner, KeyType, PublicKey, Signer, Signature}; use near_primitives::account::{AccessKey, Account, FunctionCallPermission}; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::test_utils::account_new; use near_primitives::transaction::{ - CreateAccountAction, DeleteAccountAction, DeleteKeyAction, StakeAction, TransferAction, + CreateAccountAction, DeleteAccountAction, DeleteKeyAction, StakeAction, TransferAction, NonDelegateAction, DelegateAction, }; use near_primitives::types::{AccountId, Balance, MerkleHash, StateChangeCause}; use near_primitives::version::PROTOCOL_VERSION; @@ -1510,4 +1528,52 @@ mod tests { ) .expect("valid action"); } + + #[test] + 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(()), + ); + } } diff --git a/runtime/runtime/tests/runtime_group_tools/random_config.rs b/runtime/runtime/tests/runtime_group_tools/random_config.rs index be0b3838e54..260975401bf 100644 --- a/runtime/runtime/tests/runtime_group_tools/random_config.rs +++ b/runtime/runtime/tests/runtime_group_tools/random_config.rs @@ -35,6 +35,7 @@ pub fn random_config() -> RuntimeConfig { }, delete_key_cost: random_fee(), delete_account_cost: random_fee(), + delegate_cost: random_fee(), }, storage_usage_config: StorageUsageConfig { num_bytes_account: rng.next_u64() % 10000,