diff --git a/node/src/reactor/main_reactor/tests.rs b/node/src/reactor/main_reactor/tests.rs index 16187cd9b8..891fb116fc 100644 --- a/node/src/reactor/main_reactor/tests.rs +++ b/node/src/reactor/main_reactor/tests.rs @@ -1,4 +1,5 @@ mod binary_port; +mod transactions; use std::{ collections::{BTreeMap, BTreeSet}, @@ -20,10 +21,16 @@ use tokio::time::{self, error::Elapsed}; use tracing::{error, info}; use casper_storage::{ - data_access_layer::{BidsRequest, BidsResult, TotalSupplyRequest, TotalSupplyResult}, + data_access_layer::{ + balance::{BalanceHandling, BalanceResult}, + AddressableEntityRequest, AddressableEntityResult, BalanceRequest, BidsRequest, BidsResult, + TotalSupplyRequest, TotalSupplyResult, + }, global_state::state::{StateProvider, StateReader}, }; use casper_types::{ + account::AccountHash, + crypto, execution::{ExecutionResult, ExecutionResultV2, TransformKindV2, TransformV2}, system::{ auction::{BidAddr, BidKind, BidsExt, DelegationRate}, @@ -32,9 +39,10 @@ use casper_types::{ testing::TestRng, AccountConfig, AccountsConfig, ActivationPoint, AddressableEntityHash, AvailableBlockRange, Block, BlockHash, BlockHeader, BlockV2, CLValue, Chainspec, ChainspecRawBytes, - ConsensusProtocolName, Deploy, EraId, Key, Motes, NextUpgrade, ProtocolVersion, PublicKey, - Rewards, SecretKey, StoredValue, SystemEntityRegistry, TimeDiff, Timestamp, Transaction, - TransactionHash, ValidatorConfig, U512, + ConsensusProtocolName, Deploy, EraId, FeeHandling, Gas, HoldsEpoch, Key, Motes, NextUpgrade, + PricingHandling, PricingMode, ProtocolVersion, PublicKey, RefundHandling, Rewards, SecretKey, + StoredValue, SystemEntityRegistry, TimeDiff, Timestamp, Transaction, TransactionHash, + TransactionV1Builder, ValidatorConfig, U512, }; use crate::{ @@ -104,6 +112,39 @@ struct ConfigsOverride { min_gas_price: u8, upper_threshold: u64, lower_threshold: u64, + refund_handling_override: Option, + fee_handling_override: Option, + pricing_handling_override: Option, + allow_reservations_override: Option, + balance_hold_interval_override: Option, +} + +impl ConfigsOverride { + fn with_refund_handling(mut self, refund_handling: RefundHandling) -> Self { + self.refund_handling_override = Some(refund_handling); + self + } + + fn with_fee_handling(mut self, fee_handling: FeeHandling) -> Self { + self.fee_handling_override = Some(fee_handling); + self + } + + fn with_pricing_handling(mut self, pricing_handling: PricingHandling) -> Self { + self.pricing_handling_override = Some(pricing_handling); + self + } + + #[allow(unused)] + fn with_allow_reservations(mut self, allow_reservations: bool) -> Self { + self.allow_reservations_override = Some(allow_reservations); + self + } + + fn with_balance_hold_interval(mut self, balance_hold_interval: TimeDiff) -> Self { + self.balance_hold_interval_override = Some(balance_hold_interval); + self + } } impl Default for ConfigsOverride { @@ -127,6 +168,11 @@ impl Default for ConfigsOverride { min_gas_price: 1, upper_threshold: 90, lower_threshold: 50, + refund_handling_override: None, + fee_handling_override: None, + pricing_handling_override: None, + allow_reservations_override: None, + balance_hold_interval_override: None, } } } @@ -240,6 +286,11 @@ impl TestFixture { min_gas_price: min, upper_threshold: go_up, lower_threshold: go_down, + refund_handling_override, + fee_handling_override, + pricing_handling_override, + allow_reservations_override, + balance_hold_interval_override, } = spec_override.unwrap_or_default(); if era_duration != TimeDiff::from_millis(0) { chainspec.core_config.era_duration = era_duration; @@ -265,6 +316,22 @@ impl TestFixture { chainspec.core_config.minimum_block_time * 2; chainspec.core_config.signature_rewards_max_delay = signature_rewards_max_delay; + if let Some(refund_handling) = refund_handling_override { + chainspec.core_config.refund_handling = refund_handling; + } + if let Some(fee_handling) = fee_handling_override { + chainspec.core_config.fee_handling = fee_handling; + } + if let Some(pricing_handling) = pricing_handling_override { + chainspec.core_config.pricing_handling = pricing_handling; + } + if let Some(allow_reservations) = allow_reservations_override { + chainspec.core_config.allow_reservations = allow_reservations; + } + if let Some(balance_hold_interval) = balance_hold_interval_override { + chainspec.core_config.balance_hold_interval = balance_hold_interval; + } + let mut fixture = TestFixture { rng, node_contexts: vec![], @@ -555,11 +622,30 @@ impl TestFixture { self.try_run_until( move |nodes: &Nodes| { nodes.values().all(|runner| { - runner + if runner .main_reactor() .storage() .read_execution_result(txn_hash) .is_some() + { + let exec_info = runner + .main_reactor() + .storage() + .read_execution_info(*txn_hash); + + if let Some(exec_info) = exec_info { + runner + .main_reactor() + .storage() + .read_block_header_by_height(exec_info.block_height, true) + .unwrap() + .is_some() + } else { + false + } + } else { + false + } }) }, within, diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs new file mode 100644 index 0000000000..c4a8c3993c --- /dev/null +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -0,0 +1,286 @@ +use super::*; + +use casper_types::execution::ExecutionResultV1; + +async fn transfer_to_account>( + fixture: &mut TestFixture, + amount: A, + to: PublicKey, + from: &SecretKey, + pricing: PricingMode, + transfer_id: Option, +) -> (TransactionHash, u64, ExecutionResult) { + let chain_name = fixture.chainspec.network_config.name.clone(); + + let mut txn = Transaction::from( + TransactionV1Builder::new_transfer(amount, None, to, transfer_id) + .unwrap() + .with_initiator_addr(PublicKey::from(from)) + .with_pricing_mode(pricing) + .with_chain_name(chain_name) + .build() + .unwrap(), + ); + + txn.sign(from); + let txn_hash = txn.hash(); + + fixture.inject_transaction(txn).await; + fixture + .run_until_executed_transaction(&txn_hash, TEN_SECS) + .await; + + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let exec_info = runner + .main_reactor() + .storage() + .read_execution_info(txn_hash) + .expect("Expected transaction to be included in a block."); + + ( + txn_hash, + exec_info.block_height, + exec_info + .execution_result + .expect("Exec result should have been stored."), + ) +} + +fn get_balance( + fixture: &mut TestFixture, + account_key: &PublicKey, + block_height: Option, + get_total: bool, +) -> BalanceResult { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let protocol_version = fixture.chainspec.protocol_version(); + let block_height = block_height.unwrap_or( + runner + .main_reactor() + .storage() + .highest_complete_block_height() + .expect("missing highest completed block"), + ); + let block_header = runner + .main_reactor() + .storage() + .read_block_header_by_height(block_height, true) + .expect("failure to read block header") + .unwrap(); + let state_hash = *block_header.state_root_hash(); + let balance_handling = if get_total { + BalanceHandling::Total + } else { + let block_time = block_header.timestamp().into(); + BalanceHandling::Available { + holds_epoch: HoldsEpoch::from_block_time( + block_time, + fixture.chainspec.core_config.balance_hold_interval, + ), + } + }; + runner + .main_reactor() + .contract_runtime() + .data_access_layer() + .balance(BalanceRequest::from_public_key( + state_hash, + protocol_version, + account_key.clone(), + balance_handling, + )) +} + +#[allow(unused)] +fn get_entity(fixture: &mut TestFixture, account_key: PublicKey) -> AddressableEntityResult { + let (_node_id, runner) = fixture.network.nodes().iter().next().unwrap(); + let highest_completed_height = runner + .main_reactor() + .storage() + .highest_complete_block_height() + .expect("missing highest completed block"); + let state_hash = *runner + .main_reactor() + .storage() + .read_block_header_by_height(highest_completed_height, true) + .expect("failure to read block header") + .unwrap() + .state_root_hash(); + runner + .main_reactor() + .contract_runtime() + .data_access_layer() + .addressable_entity(AddressableEntityRequest::new( + state_hash, + AccountHash::from_public_key(&account_key, crypto::blake2b).into(), + )) +} + +fn assert_exec_result_fixed_cost( + exec_result: ExecutionResult, + expected_cost: U512, + expected_consumed_gas: Gas, +) { + match exec_result { + ExecutionResult::V2(exec_result_v2) => { + assert_eq!(exec_result_v2.cost, expected_cost); + assert_eq!(exec_result_v2.consumed, expected_consumed_gas); + } + _ => { + panic!("Unexpected exec result version.") + } + } +} + +// Returns `true` is the execution result is a success. +pub fn exec_result_is_success(exec_result: &ExecutionResult) -> bool { + match exec_result { + ExecutionResult::V2(execution_result_v2) => execution_result_v2.error_message.is_none(), + ExecutionResult::V1(ExecutionResultV1::Success { .. }) => true, + ExecutionResult::V1(ExecutionResultV1::Failure { .. }) => false, + } +} + +#[tokio::test] +async fn transfer_cost_fixed_price_no_fee_no_refund() { + const TRANSFER_AMOUNT: u64 = 30_000_000_000; + + let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); + + let config = ConfigsOverride::default() + .with_pricing_handling(PricingHandling::Fixed) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::NoFee) + .with_balance_hold_interval(TimeDiff::from_seconds(5)); + + let mut fixture = TestFixture::new(initial_stakes, Some(config)).await; + + let alice_secret_key = Arc::clone(&fixture.node_contexts[0].secret_key); + let alice_public_key = PublicKey::from(&*alice_secret_key); + let charlie_secret_key = Arc::new(SecretKey::random(&mut fixture.rng)); + let charlie_public_key = PublicKey::from(&*charlie_secret_key); + + // Wait for all nodes to complete era 0. + fixture.run_until_consensus_in_era(ERA_ONE, ONE_MIN).await; + + let alice_initial_balance = *get_balance(&mut fixture, &alice_public_key, None, true) + .motes() + .expect("Expected Alice to have a balance."); + + let (_txn_hash, block_height, exec_result) = transfer_to_account( + &mut fixture, + TRANSFER_AMOUNT, + PublicKey::from(&*charlie_secret_key), + &alice_secret_key, + PricingMode::Fixed { + gas_price_tolerance: 1, + }, + Some(0xDEADBEEF), + ) + .await; + + let expected_transfer_gas = fixture + .chainspec + .system_costs_config + .mint_costs() + .transfer + .into(); + let expected_transfer_cost = expected_transfer_gas; // since we set gas_price_tolerance to 1. + + assert_exec_result_fixed_cost( + exec_result, + expected_transfer_cost, + Gas::new(expected_transfer_gas), + ); + + let alice_available_balance = + get_balance(&mut fixture, &alice_public_key, Some(block_height), false); + let alice_total_balance = + get_balance(&mut fixture, &alice_public_key, Some(block_height), true); + + // since FeeHandling is set to NoFee, we expect that there's a hold on Alice's balance for the + // cost of the transfer. The total balance of Alice now should be the initial balance - the + // amount transfered to Charlie. + let alice_expected_total_balance = alice_initial_balance - TRANSFER_AMOUNT; + // The available balance is the initial balance - the amount transferred to Charlie - the hold + // for the transfer cost. + let alice_expected_available_balance = alice_expected_total_balance - expected_transfer_cost; + + assert_eq!( + alice_total_balance + .motes() + .expect("Expected Alice to have a balance") + .clone(), + alice_expected_total_balance + ); + assert_eq!( + alice_available_balance + .motes() + .expect("Expected Alice to have a balance") + .clone(), + alice_expected_available_balance + ); + + let charlie_balance = get_balance(&mut fixture, &charlie_public_key, Some(block_height), false); + assert_eq!( + charlie_balance + .motes() + .expect("Expected Alice to have a balance") + .clone(), + TRANSFER_AMOUNT.into() + ); + + // Check if the hold is released. + let hold_release_block_height = block_height + 8; // Block time is 1s. + fixture + .run_until_block_height(hold_release_block_height, ONE_MIN) + .await; + + let alice_available_balance = get_balance( + &mut fixture, + &alice_public_key, + Some(hold_release_block_height), + false, + ); + let alice_total_balance = get_balance( + &mut fixture, + &alice_public_key, + Some(hold_release_block_height), + true, + ); + + assert_eq!(alice_available_balance.motes(), alice_total_balance.motes()); +} + +#[tokio::test] +async fn should_accept_transfer_without_id() { + let initial_stakes = InitialStakes::FromVec(vec![u128::MAX, 1]); + + let config = ConfigsOverride::default().with_pricing_handling(PricingHandling::Fixed); + let mut fixture = TestFixture::new(initial_stakes, Some(config)).await; + let transfer_amount = fixture + .chainspec + .transaction_config + .native_transfer_minimum_motes + + 100; + + let alice_secret_key = Arc::clone(&fixture.node_contexts[0].secret_key); + let charlie_secret_key = Arc::new(SecretKey::random(&mut fixture.rng)); + + // Wait for all nodes to complete era 0. + fixture.run_until_consensus_in_era(ERA_ONE, ONE_MIN).await; + + let (_, _, result) = transfer_to_account( + &mut fixture, + transfer_amount, + PublicKey::from(&*charlie_secret_key), + &alice_secret_key, + PricingMode::Fixed { + gas_price_tolerance: 1, + }, + None, + ) + .await; + + assert!(exec_result_is_success(&result)) +} diff --git a/storage/src/system/transfer.rs b/storage/src/system/transfer.rs index 78e319a434..f54db564fd 100644 --- a/storage/src/system/transfer.rs +++ b/storage/src/system/transfer.rs @@ -401,11 +401,11 @@ impl TransferRuntimeArgsBuilder { } fn resolve_id(&self) -> Result, TransferError> { - let id_value = self - .inner - .get(mint::ARG_ID) - .ok_or_else(|| TransferError::MissingArgument)?; - let id: Option = self.map_cl_value(id_value)?; + let id: Option = if let Some(id_value) = self.inner.get(mint::ARG_ID) { + self.map_cl_value(id_value)? + } else { + None + }; Ok(id) }