From ce474ebf849e5ff207b7b04e3dc54af1e058d283 Mon Sep 17 00:00:00 2001 From: Egor Ulesykiy Date: Tue, 30 Aug 2022 00:02:02 +0300 Subject: [PATCH] Draft implementation of DelegateAction (NEP-366) This is a draft implementation of NEP-366 (https://github.com/near/NEPs/pull/366) Not done yet: * Need to implement DelegateAction for implicit accounts (nonce problem) * Need new error codes for DelegateAction * Implement Fees for DelegateAction * Add tests --- chain/chain/src/test_utils.rs | 1 + chain/rosetta-rpc/src/adapters/mod.rs | 1 + core/primitives/src/receipt.rs | 30 ++++- core/primitives/src/transaction.rs | 40 ++++++ core/primitives/src/views.rs | 23 +++- .../genesis-csv-to-json/src/csv_parser.rs | 1 + .../src/tests/standard_cases/mod.rs | 1 + runtime/runtime/src/actions.rs | 50 +++++++- runtime/runtime/src/balance_checker.rs | 4 +- runtime/runtime/src/config.rs | 2 + runtime/runtime/src/lib.rs | 28 ++++- runtime/runtime/src/state_viewer/mod.rs | 1 + runtime/runtime/src/verifier.rs | 118 ++++++++++++++++++ 13 files changed, 291 insertions(+), 9 deletions(-) diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index b29b54641d5..fc90a60d58f 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -861,6 +861,7 @@ impl RuntimeAdapter for KeyValueRuntime { receipt_id: create_receipt_nonce(from.clone(), to.clone(), amount, nonce), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: from.clone(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price, output_data_receivers: vec![], diff --git a/chain/rosetta-rpc/src/adapters/mod.rs b/chain/rosetta-rpc/src/adapters/mod.rs index c3322cf308c..a3c56d5ac0c 100644 --- a/chain/rosetta-rpc/src/adapters/mod.rs +++ b/chain/rosetta-rpc/src/adapters/mod.rs @@ -410,6 +410,7 @@ impl From for Vec { ); operations.push(deploy_contract_operation); } + near_primitives::transaction::Action::Delegate(_) => todo!(), } } operations diff --git a/core/primitives/src/receipt.rs b/core/primitives/src/receipt.rs index 60e3345d507..c7ab867423b 100644 --- a/core/primitives/src/receipt.rs +++ b/core/primitives/src/receipt.rs @@ -10,7 +10,7 @@ use crate::borsh::maybestd::collections::HashMap; use crate::hash::CryptoHash; use crate::logging; use crate::serialize::{dec_format, option_base64_format}; -use crate::transaction::{Action, TransferAction}; +use crate::transaction::{Action, DelegateAction, TransferAction}; use crate::types::{AccountId, Balance, ShardId}; /// Receipts are used for a cross-shard communication. @@ -52,6 +52,7 @@ impl Receipt { receipt: ReceiptEnum::Action(ActionReceipt { signer_id: "system".parse().unwrap(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price: 0, output_data_receivers: vec![], @@ -79,6 +80,7 @@ impl Receipt { receipt: ReceiptEnum::Action(ActionReceipt { signer_id: receiver_id.clone(), + publisher_id: None, signer_public_key, gas_price: 0, output_data_receivers: vec![], @@ -87,6 +89,30 @@ impl Receipt { }), } } + + pub fn new_delegate_actions( + publisher_id: &AccountId, + predecessor_id: &AccountId, + delegate_action: &DelegateAction, + public_key: &PublicKey, + gas_price: Balance, + ) -> Self { + Receipt { + predecessor_id: predecessor_id.clone(), + receiver_id: delegate_action.reciever_id.clone(), + receipt_id: CryptoHash::default(), + + receipt: ReceiptEnum::Action(ActionReceipt { + signer_id: predecessor_id.clone(), + publisher_id: Some(publisher_id.clone()), + signer_public_key: public_key.clone(), + gas_price: gas_price, + output_data_receivers: vec![], + input_data_ids: vec![], + actions: delegate_action.actions.clone(), + }), + } + } } /// Receipt could be either ActionReceipt or DataReceipt @@ -103,6 +129,8 @@ pub enum ReceiptEnum { pub struct ActionReceipt { /// A signer of the original transaction pub signer_id: AccountId, + /// A publisher's identifier in case of a delgated action + pub publisher_id: Option, /// An access key which was used to sign the original transaction pub signer_public_key: PublicKey, /// A gas_price which has been used to buy gas in the original transaction diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index c06cd8a1f2b..321dc05757b 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -1,6 +1,7 @@ use std::borrow::Borrow; use std::fmt; use std::hash::{Hash, Hasher}; +use std::io::Error; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; @@ -70,6 +71,7 @@ pub enum Action { AddKey(AddKeyAction), DeleteKey(DeleteKeyAction), DeleteAccount(DeleteAccountAction), + Delegate(SignedDelegateAction), } impl Action { @@ -83,6 +85,10 @@ impl Action { match self { Action::FunctionCall(a) => a.deposit, Action::Transfer(a) => a.deposit, + Action::Delegate(a) => { + let delegate_action = a.get_delegate_action().unwrap(); + delegate_action.deposit + } _ => 0, } } @@ -220,6 +226,40 @@ impl From for Action { } } +#[cfg_attr(feature = "deepsize_feature", derive(deepsize::DeepSizeOf))] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct DelegateAction { + pub reciever_id: AccountId, + pub deposit: Balance, + pub nonce: u64, + pub actions: Vec, +} +#[cfg_attr(feature = "deepsize_feature", derive(deepsize::DeepSizeOf))] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct SignedDelegateAction { + // Borsh doesn't support recursive types. Therefore this field + // is deserialized to DelegateAction in runtime + pub delegate_action_serde: Vec, + pub public_key: PublicKey, + pub signature: Signature, +} + +impl SignedDelegateAction { + pub fn get_delegate_action(&self) -> Result { + DelegateAction::try_from_slice(&self.delegate_action_serde) + } + + pub fn get_hash(&self) -> CryptoHash { + hash(&self.delegate_action_serde) + } +} + +impl From for Action { + fn from(delegate_action: SignedDelegateAction) -> Self { + Self::Delegate(delegate_action) + } +} + #[cfg_attr(feature = "deepsize_feature", derive(deepsize::DeepSizeOf))] #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Eq, Debug, Clone)] #[borsh_init(init)] diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 2594023b938..1ae0586d681 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -34,7 +34,8 @@ use crate::sharding::{ use crate::transaction::{ Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, ExecutionMetadata, ExecutionOutcome, ExecutionOutcomeWithIdAndProof, - ExecutionStatus, FunctionCallAction, SignedTransaction, StakeAction, TransferAction, + ExecutionStatus, FunctionCallAction, SignedDelegateAction, SignedTransaction, StakeAction, + TransferAction, }; use crate::types::{ AccountId, AccountWithPublicKey, Balance, BlockHeight, CompiledContractCache, EpochHeight, @@ -954,6 +955,11 @@ pub enum ActionView { DeleteAccount { beneficiary_id: AccountId, }, + Delegate { + delegate_action_serde: Vec, + signature: Signature, + public_key: PublicKey, + }, } impl From for ActionView { @@ -982,6 +988,11 @@ impl From for ActionView { Action::DeleteAccount(action) => { ActionView::DeleteAccount { beneficiary_id: action.beneficiary_id } } + Action::Delegate(action) => ActionView::Delegate { + delegate_action_serde: action.delegate_action_serde, + signature: action.signature, + public_key: action.public_key, + }, } } } @@ -1011,6 +1022,15 @@ impl TryFrom for Action { ActionView::DeleteAccount { beneficiary_id } => { Action::DeleteAccount(DeleteAccountAction { beneficiary_id }) } + ActionView::Delegate { + delegate_action_serde: delegate_action, + signature, + public_key, + } => Action::Delegate(SignedDelegateAction { + delegate_action_serde: delegate_action, + signature, + public_key, + }), }) } } @@ -1478,6 +1498,7 @@ impl TryFrom for Receipt { actions, } => ReceiptEnum::Action(ActionReceipt { signer_id, + publisher_id: None, signer_public_key, gas_price, output_data_receivers: output_data_receivers diff --git a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs index f0e617be8bb..6af9f6b0570 100644 --- a/genesis-tools/genesis-csv-to-json/src/csv_parser.rs +++ b/genesis-tools/genesis-csv-to-json/src/csv_parser.rs @@ -255,6 +255,7 @@ fn account_records(row: &Row, gas_price: Balance) -> Vec { receipt_id: hash(row.account_id.as_ref().as_bytes()), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: row.account_id.clone(), + publisher_id: None, // `signer_public_key` can be anything because the key checks are not applied when // a transaction is already converted to a receipt. signer_public_key: PublicKey::empty(KeyType::ED25519), diff --git a/integration-tests/src/tests/standard_cases/mod.rs b/integration-tests/src/tests/standard_cases/mod.rs index 6b0bf330f77..c7ac7cd2b30 100644 --- a/integration-tests/src/tests/standard_cases/mod.rs +++ b/integration-tests/src/tests/standard_cases/mod.rs @@ -1414,6 +1414,7 @@ fn make_write_key_value_action(key: Vec, value: Vec) -> Action { fn make_receipt(node: &impl Node, actions: Vec, receiver_id: AccountId) -> Receipt { let receipt_enum = ReceiptEnum::Action(ActionReceipt { signer_id: alice_account(), + publisher_id: None, signer_public_key: node.signer().as_ref().public_key(), gas_price: 0, output_data_receivers: vec![], diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index d6bf5fb7aba..dfa58041422 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -11,7 +11,7 @@ use near_primitives::runtime::config::AccountCreationConfig; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::transaction::{ Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, - FunctionCallAction, StakeAction, TransferAction, + FunctionCallAction, SignedDelegateAction, StakeAction, TransferAction, }; use near_primitives::types::validator_stake::ValidatorStake; use near_primitives::types::{AccountId, BlockHeight, EpochInfoProvider, TrieCacheMode}; @@ -30,6 +30,7 @@ use near_vm_errors::{ use near_vm_logic::types::PromiseResult; use near_vm_logic::VMContext; +use crate::balance_checker::receipt_cost; use crate::config::{safe_add_gas, RuntimeConfig}; use crate::ext::{ExternalError, RuntimeExt}; use crate::{ActionResult, ApplyState}; @@ -253,6 +254,7 @@ pub(crate) fn action_function_call( receipt_id: CryptoHash::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: action_receipt.signer_id.clone(), + publisher_id: action_receipt.publisher_id.clone(), signer_public_key: action_receipt.signer_public_key.clone(), gas_price: action_receipt.gas_price, output_data_receivers: receipt.output_data_receivers, @@ -613,6 +615,46 @@ pub(crate) fn action_add_key( Ok(()) } +pub(crate) fn action_delegate_action( + apply_state: &ApplyState, + action_receipt: &ActionReceipt, + predecessor_id: &AccountId, + signed_delegate_action: &SignedDelegateAction, + result: &mut ActionResult, +) -> Result<(), RuntimeError> { + match signed_delegate_action.get_delegate_action() { + Ok(delegate_action) => { + let new_receipt = Receipt::new_delegate_actions( + &action_receipt.signer_id, + predecessor_id, + &delegate_action, + &signed_delegate_action.public_key, + action_receipt.gas_price, + ); + + let transaction_costs = &apply_state.config.transaction_costs; + let current_protocol_version = apply_state.current_protocol_version; + let cost = receipt_cost(transaction_costs, current_protocol_version, &new_receipt)?; + + if let Some(refund) = delegate_action.deposit.checked_sub(cost.clone()) { + let refund_receipt = Receipt::new_balance_refund(&action_receipt.signer_id, refund); + + result.new_receipts.push(new_receipt); + result.new_receipts.push(refund_receipt); + } else { + result.result = Err(ActionErrorKind::LackBalanceForState { + account_id: action_receipt.signer_id.clone(), + amount: cost.clone(), + } + .into()); + } + } + Err(_) => todo!(), + } + + Ok(()) +} + pub(crate) fn check_actor_permissions( action: &Action, account: &Option, @@ -645,7 +687,10 @@ pub(crate) fn check_actor_permissions( .into()); } } - Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), + Action::CreateAccount(_) + | Action::FunctionCall(_) + | Action::Transfer(_) + | Action::Delegate(_) => (), }; Ok(()) } @@ -720,6 +765,7 @@ pub(crate) fn check_account_existence( .into()); } } + Action::Delegate(_) => (), }; Ok(()) } diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 2a6b6c3ff92..bd1161d71e8 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -35,7 +35,7 @@ fn get_delayed_receipts( } /// Calculates and returns cost of a receipt. -fn receipt_cost( +pub(crate) fn receipt_cost( transaction_costs: &RuntimeFeesConfig, current_protocol_version: ProtocolVersion, receipt: &Receipt, @@ -422,6 +422,7 @@ mod tests { receipt_id: Default::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: tx.transaction.signer_id.clone(), + publisher_id: None, signer_public_key: tx.transaction.public_key.clone(), gas_price, output_data_receivers: vec![], @@ -477,6 +478,7 @@ mod tests { receipt_id: Default::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: tx.transaction.signer_id.clone(), + publisher_id: None, signer_public_key: tx.transaction.public_key.clone(), gas_price, output_data_receivers: vec![], diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index ee133a8788b..9ce34e02a04 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -120,6 +120,7 @@ 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(_) => 0, // TODO: Set some fee }; result = safe_add_gas(result, delta)?; } @@ -170,6 +171,7 @@ pub fn exec_fee( }, DeleteKey(_) => cfg.delete_key_cost.exec_fee(), DeleteAccount(_) => cfg.delete_account_cost.exec_fee(), + Delegate(_) => cfg.delete_account_cost.exec_fee(), // TODO: Add another fee for Delegate action. } } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 93b3302e9c7..fdfd575d6bd 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -251,6 +251,7 @@ impl Runtime { receipt_id, receipt: ReceiptEnum::Action(ActionReceipt { signer_id: transaction.signer_id.clone(), + publisher_id: None, signer_public_key: transaction.public_key.clone(), gas_price: verification_result.receipt_gas_price, output_data_receivers: vec![], @@ -442,6 +443,15 @@ impl Runtime { apply_state.current_protocol_version, )?; } + Action::Delegate(signed_delegate_action) => { + action_delegate_action( + apply_state, + action_receipt, + account_id, + signed_delegate_action, + &mut result, + )?; + } }; Ok(result) } @@ -792,16 +802,22 @@ impl Runtime { )?, )?; } + + let signer_is_predecessor = + action_receipt.signer_id.as_str() == receipt.predecessor_id.as_str(); + let refund_id = if action_receipt.publisher_id.is_some() && signer_is_predecessor { + action_receipt.publisher_id.as_ref().unwrap() + } else { + &receipt.predecessor_id + }; if deposit_refund > 0 { - result - .new_receipts - .push(Receipt::new_balance_refund(&receipt.predecessor_id, deposit_refund)); + result.new_receipts.push(Receipt::new_balance_refund(refund_id, deposit_refund)); } if gas_balance_refund > 0 { // Gas refunds refund the allowance of the access key, so if the key exists on the // account it will increase the allowance by the refund amount. result.new_receipts.push(Receipt::new_gas_refund( - &action_receipt.signer_id, + refund_id, gas_balance_refund, action_receipt.signer_public_key.clone(), )); @@ -1505,6 +1521,7 @@ mod tests { receipt_id: CryptoHash::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: account_id, + publisher_id: None, signer_public_key: signer.public_key(), gas_price: GAS_PRICE, output_data_receivers: vec![], @@ -1855,6 +1872,7 @@ mod tests { receipt_id, receipt: ReceiptEnum::Action(ActionReceipt { signer_id: bob_account(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price: GAS_PRICE, output_data_receivers: vec![], @@ -2170,6 +2188,7 @@ mod tests { receipt_id: CryptoHash::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: bob_account(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price, output_data_receivers: vec![], @@ -2239,6 +2258,7 @@ mod tests { receipt_id: CryptoHash::default(), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: bob_account(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price, output_data_receivers: vec![], diff --git a/runtime/runtime/src/state_viewer/mod.rs b/runtime/runtime/src/state_viewer/mod.rs index 1f72d098bb1..c1b48ec2d1c 100644 --- a/runtime/runtime/src/state_viewer/mod.rs +++ b/runtime/runtime/src/state_viewer/mod.rs @@ -212,6 +212,7 @@ impl TrieViewer { }; let action_receipt = ActionReceipt { signer_id: originator_id.clone(), + publisher_id: None, signer_public_key: public_key, gas_price: 0, output_data_receivers: vec![], diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index 3c43505bbc3..59eecf98308 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -1,5 +1,7 @@ use near_crypto::key_conversion::is_valid_staking_key; +use near_crypto::PublicKey; use near_primitives::runtime::get_insufficient_storage_stake; +use near_primitives::transaction::Transaction; use near_primitives::{ account::AccessKeyPermission, config::VMLimitConfig, @@ -92,6 +94,14 @@ pub fn verify_and_charge_transaction( let transaction = &signed_transaction.transaction; let signer_id = &transaction.signer_id; + verify_delegate_action( + state_update, + &config.wasm_config.limit_config, + transaction, + block_height, + current_protocol_version, + )?; + let mut signer = match get_account(state_update, signer_id)? { Some(signer) => signer, None => { @@ -329,7 +339,114 @@ pub fn validate_action( Action::AddKey(a) => validate_add_key_action(limit_config, a), Action::DeleteKey(_) => Ok(()), Action::DeleteAccount(_) => Ok(()), + Action::Delegate(_) => Ok(()), + } +} + +fn verify_delegate_action( + state_update: &mut TrieUpdate, + limit_config: &VMLimitConfig, + transaction: &Transaction, + #[allow(unused)] block_height: Option, + current_protocol_version: ProtocolVersion, +) -> Result<(), RuntimeError> { + let mut iter = transaction.actions.iter().peekable(); + let mut found_delegate_action = false; + while let Some(action) = iter.next() { + if let Action::Delegate(signed_delegate_action) = action { + if !found_delegate_action { + // There should be only one DelegateAction + found_delegate_action = true; + + let hash = signed_delegate_action.get_hash(); + let public_key = &signed_delegate_action.public_key; + if !signed_delegate_action.signature.verify(hash.as_ref(), public_key) { + return Err(InvalidTxError::InvalidSignature) + .map_err(RuntimeError::InvalidTxError); + } + + let delegate_action = signed_delegate_action.get_delegate_action().unwrap(); + validate_actions(limit_config, &delegate_action.actions) + .map_err(InvalidTxError::ActionsValidation)?; + + // DelegateAction shouldn't contain a nested DelegateAction + if delegate_action.actions.iter().any(|a| matches!(a, Action::Delegate(_))) { + return Err(ActionsValidationError::TotalNumberOfActionsExceeded { + total_number_of_actions: transaction.actions.len() as u64, + limit: 0, + }) + .map_err(InvalidTxError::ActionsValidation) + .map_err(RuntimeError::InvalidTxError); + } + + validate_access_key( + state_update, + &transaction.receiver_id, + &signed_delegate_action.public_key, + delegate_action.nonce, + block_height, + current_protocol_version, + )?; + } else { + return Err(ActionsValidationError::TotalNumberOfActionsExceeded { + total_number_of_actions: transaction.actions.len() as u64, + limit: 1, + }) + .map_err(InvalidTxError::ActionsValidation) + .map_err(RuntimeError::InvalidTxError); + } + } } + + Ok(()) +} + +pub fn validate_access_key( + state_update: &mut TrieUpdate, + signer_id: &AccountId, + public_key: &PublicKey, + nonce: u64, + #[allow(unused)] block_height: Option, + current_protocol_version: ProtocolVersion, +) -> Result<(), RuntimeError> { + match get_account(state_update, signer_id)? { + Some(signer) => signer, + None => { + return Err(InvalidTxError::SignerDoesNotExist { signer_id: signer_id.clone() }.into()); + } + }; + let mut access_key = match get_access_key(state_update, signer_id, public_key)? { + Some(access_key) => access_key, + None => { + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: signer_id.clone(), + public_key: public_key.clone(), + }, + ) + .into()); + } + }; + + if nonce <= access_key.nonce { + return Err( + InvalidTxError::InvalidNonce { tx_nonce: nonce, ak_nonce: access_key.nonce }.into() + ); + } + if checked_feature!("stable", AccessKeyNonceRange, current_protocol_version) { + if let Some(height) = block_height { + let upper_bound = + height * near_primitives::account::AccessKey::ACCESS_KEY_NONCE_RANGE_MULTIPLIER; + if nonce >= upper_bound { + return Err(InvalidTxError::NonceTooLarge { tx_nonce: nonce, upper_bound }.into()); + } + } + }; + + access_key.nonce = nonce; + + set_access_key(state_update, signer_id.clone(), public_key.clone(), &access_key); + Ok(()) } /// Validates `DeployContractAction`. Checks that the given contract size doesn't exceed the limit. @@ -1202,6 +1319,7 @@ mod tests { &limit_config, &ActionReceipt { signer_id: alice_account(), + publisher_id: None, signer_public_key: PublicKey::empty(KeyType::ED25519), gas_price: 100, output_data_receivers: vec![],