diff --git a/crates/client/src/rpc.rs b/crates/client/src/rpc.rs index 89ddb3d90a..5d3b0bee85 100644 --- a/crates/client/src/rpc.rs +++ b/crates/client/src/rpc.rs @@ -36,7 +36,8 @@ mod traits; pub mod types; pub use cfxcore::rpc_errors::{ - BoxFuture as RpcBoxFuture, Error as RpcError, ErrorKind as RpcErrorKind, + invalid_params, invalid_params_check, BoxFuture as RpcBoxFuture, + Error as RpcError, ErrorKind as RpcErrorKind, ErrorKind::JsonRpcError as JsonRpcErrorKind, Result as RpcResult, }; diff --git a/crates/client/src/rpc/impls/cfx/cfx_handler.rs b/crates/client/src/rpc/impls/cfx/cfx_handler.rs index d49a200936..a28e1d5257 100644 --- a/crates/client/src/rpc/impls/cfx/cfx_handler.rs +++ b/crates/client/src/rpc/impls/cfx/cfx_handler.rs @@ -6,7 +6,7 @@ use crate::rpc::{ error_codes::{internal_error_msg, invalid_params_msg}, types::{ call_request::rpc_call_request_network, - errors::check_rpc_address_network, pos::PoSEpochReward, FeeHistory, + errors::check_rpc_address_network, pos::PoSEpochReward, CfxFeeHistory, PoSEconomics, RpcAddress, SponsorInfo, StatOnGasLoad, TokenSupplyInfo, VoteParamsInfo, WrapTransaction, U64 as HexU64, }, @@ -47,7 +47,7 @@ use network::{ }; use parking_lot::Mutex; use primitives::{ - filter::LogFilter, receipt::EVM_SPACE_SUCCESS, Account, Block, + filter::LogFilter, receipt::EVM_SPACE_SUCCESS, Account, Block, BlockHeader, BlockReceipts, DepositInfo, SignedTransaction, StorageKey, StorageRoot, StorageValue, Transaction, TransactionIndex, TransactionStatus, TransactionWithSignature, VoteStakeInfo, @@ -115,7 +115,7 @@ pub(crate) struct BlockExecInfo { pub(crate) block: Arc, pub(crate) epoch_number: u64, pub(crate) maybe_state_root: Option, - pub(crate) pivot_hash: H256, + pub(crate) pivot_header: Arc, } pub struct RpcImpl { @@ -790,12 +790,23 @@ impl RpcImpl { bail!("Inconsistent state"); } + let pivot_header = if let Some(x) = self + .consensus + .get_data_manager() + .block_header_by_hash(&pivot_hash) + { + x + } else { + warn!("Cannot find pivot header when get block execution info: pivot hash {:?}", pivot_hash); + return Ok(None); + }; + Ok(Some(BlockExecInfo { block_receipts, block, epoch_number, maybe_state_root, - pivot_hash, + pivot_header, })) } @@ -840,7 +851,7 @@ impl RpcImpl { prior_gas_used, Some(exec_info.epoch_number), exec_info.block_receipts.block_number, - exec_info.block.block_header.base_price(), + exec_info.pivot_header.base_price(), exec_info.maybe_state_root.clone(), tx_exec_error_msg, *self.sync.network.get_network_type(), @@ -900,10 +911,10 @@ impl RpcImpl { }; // pivot chain reorg - if pivot_assumption != exec_info.pivot_hash { + if pivot_assumption != exec_info.pivot_header.hash() { bail!(pivot_assumption_failed( pivot_assumption, - exec_info.pivot_hash + exec_info.pivot_header.hash() )); } @@ -2272,7 +2283,7 @@ impl Cfx for CfxHandler { fn account_pending_info(&self, addr: RpcAddress) -> BoxFuture>; fn account_pending_transactions(&self, address: RpcAddress, maybe_start_nonce: Option, maybe_limit: Option) -> BoxFuture; fn get_pos_reward_by_epoch(&self, epoch: EpochNumber) -> JsonRpcResult>; - fn fee_history(&self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec) -> BoxFuture; + fn fee_history(&self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec) -> BoxFuture; fn max_priority_fee_per_gas(&self) -> BoxFuture; } diff --git a/crates/client/src/rpc/impls/cfx/common.rs b/crates/client/src/rpc/impls/cfx/common.rs index b9ca7168d7..c4939cfcf9 100644 --- a/crates/client/src/rpc/impls/cfx/common.rs +++ b/crates/client/src/rpc/impls/cfx/common.rs @@ -14,10 +14,10 @@ use crate::rpc::{ types::{ errors::check_rpc_address_network, pos::PoSEpochReward, AccountPendingInfo, AccountPendingTransactions, Block as RpcBlock, - BlockHashOrEpochNumber, Bytes, CheckBalanceAgainstTransactionResponse, - EpochNumber, FeeHistory, RpcAddress, Status as RpcStatus, - Transaction as RpcTransaction, TxPoolPendingNonceRange, TxPoolStatus, - TxWithPoolInfo, U64 as HexU64, + BlockHashOrEpochNumber, Bytes, CfxFeeHistory, + CheckBalanceAgainstTransactionResponse, EpochNumber, FeeHistory, + RpcAddress, Status as RpcStatus, Transaction as RpcTransaction, + TxPoolPendingNonceRange, TxPoolStatus, TxWithPoolInfo, U64 as HexU64, }, RpcErrorKind, RpcResult, }; @@ -529,17 +529,25 @@ impl RpcImpl { ) } + // TODO: cache the history to improve performance pub fn fee_history( &self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec, - ) -> RpcResult { + ) -> RpcResult { + if newest_block == EpochNumber::LatestMined { + return Err(RpcError::invalid_params( + "newestBlock cannot be 'LatestMined'", + ) + .into()); + } + info!( "RPC Request: cfx_feeHistory: block_count={}, newest_block={:?}, reward_percentiles={:?}", block_count, newest_block, reward_percentiles ); if block_count.as_u64() == 0 { - return Ok(FeeHistory::new()); + return Ok(FeeHistory::new().to_cfx_fee_history()); } // keep read lock to ensure consistent view let inner = self.consensus_graph().inner.read(); @@ -594,21 +602,26 @@ impl RpcImpl { .map_err(|_| RpcError::internal_error())?; if current_height == 0 { - fee_history.finish(0, None, Space::Native); - return Ok(fee_history); + break; } else { current_height -= 1; } } - let block = fetch_block(current_height)?; + // Fetch the block after the last block in the history + let block = fetch_block(start_height + 1)?; + let oldest_block = if current_height == 0 { + 0 + } else { + current_height + 1 + }; fee_history.finish( - current_height + 1, + oldest_block, block.block_header.base_price().as_ref(), Space::Native, ); - Ok(fee_history) + Ok(fee_history.to_cfx_fee_history()) } pub fn max_priority_fee_per_gas(&self) -> RpcResult { diff --git a/crates/client/src/rpc/impls/cfx/light.rs b/crates/client/src/rpc/impls/cfx/light.rs index 1fb19b00e8..3c3c210b81 100644 --- a/crates/client/src/rpc/impls/cfx/light.rs +++ b/crates/client/src/rpc/impls/cfx/light.rs @@ -42,15 +42,15 @@ use crate::{ pos::{Block as PosBlock, PoSEpochReward}, Account as RpcAccount, AccountPendingInfo, AccountPendingTransactions, BlameInfo, Block as RpcBlock, - BlockHashOrEpochNumber, Bytes, CallRequest, CfxRpcLogFilter, - CheckBalanceAgainstTransactionResponse, ConsensusGraphStates, - EpochNumber, EstimateGasAndCollateralResponse, FeeHistory, - Log as RpcLog, PoSEconomics, Receipt as RpcReceipt, - RewardInfo as RpcRewardInfo, RpcAddress, SendTxRequest, - SponsorInfo, StatOnGasLoad, Status as RpcStatus, - StorageCollateralInfo, SyncGraphStates, TokenSupplyInfo, - Transaction as RpcTransaction, VoteParamsInfo, WrapTransaction, - U64 as HexU64, + BlockHashOrEpochNumber, Bytes, CallRequest, CfxFeeHistory, + CfxRpcLogFilter, CheckBalanceAgainstTransactionResponse, + ConsensusGraphStates, EpochNumber, + EstimateGasAndCollateralResponse, FeeHistory, Log as RpcLog, + PoSEconomics, Receipt as RpcReceipt, RewardInfo as RpcRewardInfo, + RpcAddress, SendTxRequest, SponsorInfo, StatOnGasLoad, + Status as RpcStatus, StorageCollateralInfo, SyncGraphStates, + TokenSupplyInfo, Transaction as RpcTransaction, VoteParamsInfo, + WrapTransaction, U64 as HexU64, }, RpcBoxFuture, RpcResult, }, @@ -1094,14 +1094,18 @@ impl RpcImpl { fn fee_history( &self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec, - ) -> RpcBoxFuture { + ) -> RpcBoxFuture { info!( "RPC Request: cfx_feeHistory: block_count={}, newest_block={:?}, reward_percentiles={:?}", block_count, newest_block, reward_percentiles ); if block_count.as_u64() == 0 { - return Box::new(async { Ok(FeeHistory::new()) }.boxed().compat()); + return Box::new( + async { Ok(FeeHistory::new().to_cfx_fee_history()) } + .boxed() + .compat(), + ); } // clone to avoid lifetime issues due to capturing `self` @@ -1145,8 +1149,7 @@ impl RpcImpl { .map_err(|_| RpcError::internal_error())?; if current_height == 0 { - fee_history.finish(0, None, Space::Native); - return Ok(fee_history); + break; } else { current_height -= 1; } @@ -1155,15 +1158,20 @@ impl RpcImpl { let block = fetch_block_for_fee_history( consensus_graph.clone(), light.clone(), - current_height, + start_height + 1, ) .await?; + let oldest_block = if current_height == 0 { + 0 + } else { + current_height + 1 + }; fee_history.finish( - current_height + 1, + oldest_block, block.block_header.base_price().as_ref(), Space::Native, ); - Ok(fee_history) + Ok(fee_history.to_cfx_fee_history()) }; Box::new(fut.boxed().compat()) @@ -1240,7 +1248,7 @@ impl Cfx for CfxHandler { fn transaction_by_hash(&self, hash: H256) -> BoxFuture>; fn transaction_receipt(&self, tx_hash: H256) -> BoxFuture>; fn vote_list(&self, address: RpcAddress, num: Option) -> BoxFuture>; - fn fee_history(&self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec) -> BoxFuture; + fn fee_history(&self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec) -> BoxFuture; } } diff --git a/crates/client/src/rpc/impls/eth/eth_handler.rs b/crates/client/src/rpc/impls/eth/eth_handler.rs index 36dad68811..79f94460a3 100644 --- a/crates/client/src/rpc/impls/eth/eth_handler.rs +++ b/crates/client/src/rpc/impls/eth/eth_handler.rs @@ -187,7 +187,8 @@ fn block_tx_by_index( impl EthHandler { fn exec_transaction( - &self, request: CallRequest, block_number_or_hash: Option, + &self, mut request: CallRequest, + block_number_or_hash: Option, ) -> CfxRpcResult<(ExecutionOutcome, EstimateExt)> { let consensus_graph = self.consensus_graph(); @@ -213,6 +214,9 @@ impl EthHandler { epoch => epoch.try_into()?, }; + // if gas_price is zero, it is considered as not set + request.unset_zero_gas_price(); + let estimate_request = EstimateRequest { has_sender: request.from.is_some(), has_gas_limit: request.gas.is_some(), @@ -967,16 +971,20 @@ impl Eth for EthHandler { .map_err(|_| RpcError::internal_error())?; if current_height == 0 { - fee_history.finish(0, None, Space::Ethereum); - return Ok(fee_history); + break; } else { current_height -= 1; } } - let block = fetch_block(current_height)?; + let block = fetch_block(start_height + 1)?; + let oldest_block = if current_height == 0 { + 0 + } else { + current_height + 1 + }; fee_history.finish( - current_height + 1, + oldest_block, block.pivot_header.base_price().as_ref(), Space::Ethereum, ); diff --git a/crates/client/src/rpc/traits/cfx_space/cfx.rs b/crates/client/src/rpc/traits/cfx_space/cfx.rs index 95de945952..5fe7e9071e 100644 --- a/crates/client/src/rpc/traits/cfx_space/cfx.rs +++ b/crates/client/src/rpc/traits/cfx_space/cfx.rs @@ -5,9 +5,9 @@ use crate::rpc::types::{ pos::PoSEpochReward, Account as RpcAccount, AccountPendingInfo, AccountPendingTransactions, Block, BlockHashOrEpochNumber, Bytes, - CallRequest, CfxFilterChanges, CfxRpcLogFilter, + CallRequest, CfxFeeHistory, CfxFilterChanges, CfxRpcLogFilter, CheckBalanceAgainstTransactionResponse, EpochNumber, - EstimateGasAndCollateralResponse, FeeHistory, Log as RpcLog, PoSEconomics, + EstimateGasAndCollateralResponse, Log as RpcLog, PoSEconomics, Receipt as RpcReceipt, RewardInfo as RpcRewardInfo, RpcAddress, SponsorInfo, Status as RpcStatus, StorageCollateralInfo, TokenSupplyInfo, Transaction, VoteParamsInfo, U64 as HexU64, @@ -205,7 +205,7 @@ pub trait Cfx { fn fee_history( &self, block_count: HexU64, newest_block: EpochNumber, reward_percentiles: Vec, - ) -> BoxFuture; + ) -> BoxFuture; /// Check if user balance is enough for the transaction. #[rpc(name = "cfx_checkBalanceAgainstTransaction")] diff --git a/crates/client/src/rpc/types.rs b/crates/client/src/rpc/types.rs index a5b53b75ee..02405f90d2 100644 --- a/crates/client/src/rpc/types.rs +++ b/crates/client/src/rpc/types.rs @@ -6,7 +6,6 @@ mod account; mod blame_info; mod block; mod bytes; -pub mod call_request; pub mod cfx; mod consensus_graph_states; mod epoch_number; @@ -40,11 +39,17 @@ pub use self::{ blame_info::BlameInfo, block::{Block, BlockTransactions, Header}, bytes::Bytes, - call_request::{ - sign_call, CallRequest, CheckBalanceAgainstTransactionResponse, - EstimateGasAndCollateralResponse, SendTxRequest, MAX_GAS_CALL_REQUEST, + cfx::{ + address, + address::RpcAddress, + call_request::{ + self, sign_call, CallRequest, + CheckBalanceAgainstTransactionResponse, + EstimateGasAndCollateralResponse, SendTxRequest, + MAX_GAS_CALL_REQUEST, + }, + CfxFeeHistory, }, - cfx::{address, address::RpcAddress}, consensus_graph_states::ConsensusGraphStates, epoch_number::{BlockHashOrEpochNumber, EpochNumber}, fee_history::FeeHistory, diff --git a/crates/client/src/rpc/types/call_request.rs b/crates/client/src/rpc/types/cfx/call_request.rs similarity index 100% rename from crates/client/src/rpc/types/call_request.rs rename to crates/client/src/rpc/types/cfx/call_request.rs diff --git a/crates/client/src/rpc/types/cfx/fee_history.rs b/crates/client/src/rpc/types/cfx/fee_history.rs new file mode 100644 index 0000000000..9a0696597d --- /dev/null +++ b/crates/client/src/rpc/types/cfx/fee_history.rs @@ -0,0 +1,35 @@ +use cfx_types::U256; +use std::collections::VecDeque; + +#[derive(Serialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct CfxFeeHistory { + /// Oldest epoch + oldest_epoch: U256, + /// An array of pivot block base fees per gas. This includes one block + /// earlier than the oldest block. Zeroes are returned for pre-EIP-1559 + /// blocks. + base_fee_per_gas: VecDeque, + /// In Conflux, 1559 is adjusted by the current block's gas limit of total + /// transactions, instead of parent's gas used + gas_used_ratio: VecDeque, + /// A two-dimensional array of effective priority fees per gas at the + /// requested block percentiles. + reward: VecDeque>, +} + +impl CfxFeeHistory { + pub fn new( + oldest_epoch: U256, base_fee_per_gas: VecDeque, + gas_used_ratio: VecDeque, reward: VecDeque>, + ) -> Self { + CfxFeeHistory { + oldest_epoch, + base_fee_per_gas, + gas_used_ratio, + reward, + } + } + + pub fn reward(&self) -> &VecDeque> { &self.reward } +} diff --git a/crates/client/src/rpc/types/cfx/mod.rs b/crates/client/src/rpc/types/cfx/mod.rs index 89d8fb6a46..4a15fecffb 100644 --- a/crates/client/src/rpc/types/cfx/mod.rs +++ b/crates/client/src/rpc/types/cfx/mod.rs @@ -1,5 +1,8 @@ mod access_list; pub mod address; +pub mod call_request; +mod fee_history; pub use access_list::*; pub use address::RpcAddress; +pub use fee_history::*; diff --git a/crates/client/src/rpc/types/eth/call_request.rs b/crates/client/src/rpc/types/eth/call_request.rs index d9d7906cea..3d694209b1 100644 --- a/crates/client/src/rpc/types/eth/call_request.rs +++ b/crates/client/src/rpc/types/eth/call_request.rs @@ -48,3 +48,11 @@ pub struct CallRequest { #[serde(rename = "type")] pub transaction_type: Option, } + +impl CallRequest { + pub fn unset_zero_gas_price(&mut self) { + if self.gas_price == Some(U256::zero()) { + self.gas_price = None; + } + } +} diff --git a/crates/client/src/rpc/types/fee_history.rs b/crates/client/src/rpc/types/fee_history.rs index 50a68f1344..d530701597 100644 --- a/crates/client/src/rpc/types/fee_history.rs +++ b/crates/client/src/rpc/types/fee_history.rs @@ -3,6 +3,8 @@ use std::collections::VecDeque; use cfx_types::{Space, SpaceMap, U256}; use primitives::{transaction::SignedTransaction, BlockHeader}; +use super::CfxFeeHistory; + #[derive(Serialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct FeeHistory { @@ -24,6 +26,15 @@ impl FeeHistory { pub fn reward(&self) -> &VecDeque> { &self.reward } + pub fn to_cfx_fee_history(self) -> CfxFeeHistory { + CfxFeeHistory::new( + self.oldest_block, + self.base_fee_per_gas, + self.gas_used_ratio, + self.reward, + ) + } + pub fn push_front_block<'a, I>( &mut self, space: Space, percentiles: &Vec, pivot_header: &BlockHeader, transactions: I, @@ -41,9 +52,7 @@ impl FeeHistory { return Ok(()); }; - self.base_fee_per_gas.push_front( - pivot_header.base_price().map_or(U256::zero(), |x| x[space]), - ); + self.base_fee_per_gas.push_front(base_price); let gas_limit: U256 = match space { Space::Native => pivot_header.gas_limit() * 9 / 10, @@ -74,12 +83,12 @@ impl FeeHistory { } pub fn finish( - &mut self, oldest_block: u64, - parent_base_price: Option<&SpaceMap>, space: Space, + &mut self, oldest_block: u64, last_base_price: Option<&SpaceMap>, + space: Space, ) { self.oldest_block = oldest_block.into(); self.base_fee_per_gas - .push_front(parent_base_price.map_or(U256::zero(), |x| x[space])); + .push_back(last_base_price.map_or(U256::zero(), |x| x[space])); } } diff --git a/dev-support/dep_pip3.sh b/dev-support/dep_pip3.sh index 40f7f2fe47..62559d494d 100755 --- a/dev-support/dep_pip3.sh +++ b/dev-support/dep_pip3.sh @@ -8,6 +8,7 @@ function install() { fi } +install git+https://github.com/conflux-fans/cfx-account.git@v1.1.0-beta.2 # install cfx-account lib and prepare for CIP-1559 tests install eth-utils install rlp==1.2.0 install py-ecc==5.2.0 diff --git a/tests/cip137_test.py b/tests/cip137_test.py new file mode 100644 index 0000000000..687f5ddec2 --- /dev/null +++ b/tests/cip137_test.py @@ -0,0 +1,212 @@ +import math +from typing import Union, Tuple +from conflux.rpc import RpcClient, default_config +from test_framework.util import ( + assert_equal, +) + +from cfx_account import Account as CfxAccount +from cfx_account.signers.local import LocalAccount as CfxLocalAccount + +from test_framework.test_framework import ConfluxTestFramework +from test_framework.util import ( + generate_blocks_for_base_fee_manipulation, + generate_single_block_for_base_fee_manipulation, + assert_correct_fee_computation_for_core_tx, +) + +MIN_NATIVE_BASE_PRICE = 10000 +BURNT_RATIO = 0.5 + + +class CIP137Test(ConfluxTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.conf_parameters["min_native_base_price"] = MIN_NATIVE_BASE_PRICE + self.conf_parameters["next_hardfork_transition_height"] = 1 + self.conf_parameters["next_hardfork_transition_number"] = 1 + + def setup_network(self): + self.add_nodes(self.num_nodes) + self.start_node(0, ["--archive"]) + self.rpc = RpcClient(self.nodes[0]) + + # We need to ensure that the tx in B block + # B and ending block will be in the same epoch + # --- --- --- --- --- --- + # .- | A | <--- | C | <--- | D | <--- | E | <--- | F | <--- | G | ... + # --- | --- --- --- --- --- --- + # ... <--- | P | <-* . + # --- | --- . + # .- | B | <................................................... + # --- + # ensures txs to be included in B block and the ending block (e.g. F) base gas price is greater than the specified target_minimum_base_fee (not guaranteed to be the first block) + # returns the ending block hash + def construct_non_pivot_block( + self, + acct: CfxLocalAccount, + txs: list, + starting_block_hash: str = None, + epoch_delta: int = 6, # 1.125^6 -> 2.027 which would make the initial tx invalid + ) -> Tuple[str, str]: + + if epoch_delta <= 0: + raise ValueError("epoch_delta must be positive") + + if starting_block_hash is None: + starting_block_hash = self.rpc.block_by_epoch("latest_mined")["hash"] + + # create the non-pivot block + non_pivot_block = self.rpc.generate_custom_block( + parent_hash=starting_block_hash, txs=txs, referee=[] + ) + ending_but_two_block, account_next_nonce = ( + generate_blocks_for_base_fee_manipulation( + self.rpc, acct, epoch_delta-1, initial_parent_hash=starting_block_hash + ) + ) + ending_block, _ = generate_single_block_for_base_fee_manipulation( + self.rpc, + acct, + [non_pivot_block], + parent_hash=ending_but_two_block, + starting_nonce=account_next_nonce, + ) + return non_pivot_block, ending_block + + def init_acct_with_cfx(self, drip: int = 10**21) -> CfxLocalAccount: + self.rpc.send_tx( + self.rpc.new_tx( + receiver=(acct := CfxAccount.create()).address, + value=drip, + gas_price=max( + self.rpc.base_fee_per_gas() * 2, MIN_NATIVE_BASE_PRICE + ), # avoid genisis zero gas price + ), + True, + ) + return acct + + def get_gas_charged(self, tx_hash: str) -> int: + gas_limit = int(self.rpc.get_tx(tx_hash)["gas"], 16) + gas_used = int(self.rpc.get_transaction_receipt(tx_hash)["gasUsed"], 16) + return max(int(3/4*gas_limit), gas_used) + + def run_test(self): + + acct1 = self.init_acct_with_cfx() + acct2 = self.init_acct_with_cfx() + + block_p = self.rpc.block_by_epoch("latest_mined")["hash"] + + gas_price_level_1 = MIN_NATIVE_BASE_PRICE + gas_price_level_1_5 = int(MIN_NATIVE_BASE_PRICE * 1.5) + gas_price_level_2 = self.rpc.base_fee_per_gas() * 10 + + acct1_txs = [ + self.rpc.new_typed_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct1.key, + nonce=0, + max_fee_per_gas=gas_price_level_2, + ), # expected to succeed + self.rpc.new_typed_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct1.key, + nonce=1, + max_fee_per_gas=gas_price_level_1_5, + ), # expected to succeed with max fee less than epoch base gas fee + self.rpc.new_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct1.key, + nonce=2, + gas_price=gas_price_level_1, + ), # expected to be ignored and can be resend later + self.rpc.new_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct1.key, + nonce=3, + gas_price=gas_price_level_2, + ), # expected to be ignored + ] + + acct2_txs = [ + self.rpc.new_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct2.key, + nonce=0, + gas_price=gas_price_level_2, + ), # expected to succeed + self.rpc.new_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct2.key, + nonce=1, + gas_price=gas_price_level_2, + ), # expected to succeed + self.rpc.new_tx( + receiver=self.rpc.rand_addr(), + priv_key=acct2.key, + nonce=2, + gas_price=gas_price_level_2, + ), # expected to succeed + ] + + block_b, block_f = self.construct_non_pivot_block( + CfxAccount.from_key(default_config["GENESIS_PRI_KEY"]), + [*acct1_txs, *acct2_txs], + starting_block_hash=block_p, + epoch_delta=6, # 1.125^6 -> 2.03 + ) + + self.log.info(f"current base fee per gas: {self.rpc.base_fee_per_gas()}") + + # we are ensuring the gas price order: + # gas_price_level_1 < current_base_fee * burnt_ratio < gas_price_level_1_5 < current_base_fee < gas_price_level_2 + assert gas_price_level_2 > self.rpc.base_fee_per_gas() * BURNT_RATIO + assert ( + gas_price_level_1 < self.rpc.base_fee_per_gas() * BURNT_RATIO + ), f"gas_price_level_1 {gas_price_level_1} should be less than {self.rpc.base_fee_per_gas() * BURNT_RATIO}" + + # wait for epoch of block f executed + parent_block = block_f + for _ in range(30): + block = self.rpc.generate_custom_block( + parent_hash=parent_block, referee=[], txs=[] + ) + parent_block = block + + assert_equal(self.rpc.get_nonce(acct1.address), 2) + assert_equal(self.rpc.get_nonce(acct2.address), 3) + focusing_block = self.rpc.block_by_hash(block_b, True) + epoch = int(focusing_block["epochNumber"],16) + + self.log.info(f"epoch of block b: {epoch}") + self.log.info(f"heigth of block b: {int(focusing_block['height'], 16)}") + self.log.info(f"base_fee_per_gas for epoch {epoch}: {self.rpc.base_fee_per_gas(epoch)}") + self.log.info(f"burnt_fee_per_gas for epoch {epoch}: {self.rpc.base_fee_per_gas(epoch) * 0.5}") + self.log.info(f"least base fee for epoch {epoch}: {self.rpc.base_fee_per_gas(epoch) * BURNT_RATIO}") + self.log.info(f"transactions in block b: {self.rpc.block_by_hash(block_b)['transactions']}") + + assert_equal(focusing_block["transactions"][0]["status"], "0x0") + assert_equal(focusing_block["transactions"][1]["status"], "0x0") + assert_equal(focusing_block["transactions"][2]["status"], None) + assert_equal(focusing_block["transactions"][2]["blockHash"], None) + assert_equal(focusing_block["transactions"][3]["status"], None) + assert_equal(focusing_block["transactions"][3]["blockHash"], None) + + # as comparison + assert_equal(focusing_block["transactions"][4]["status"], "0x0") + assert_equal(focusing_block["transactions"][5]["status"], "0x0") + assert_equal(focusing_block["transactions"][6]["status"], "0x0") + + for tx_hash in self.rpc.block_by_hash(block_b)['transactions']: + assert_correct_fee_computation_for_core_tx(self.rpc, tx_hash, BURNT_RATIO) + + self.rpc.generate_blocks(20, 5) + + # transactions shall be sent back to txpool and then get packed + assert_equal(self.rpc.get_nonce(acct1.address), 4) + + +if __name__ == "__main__": + CIP137Test().main() diff --git a/tests/cip1559_test.py b/tests/cip1559_test.py new file mode 100644 index 0000000000..1c08166e27 --- /dev/null +++ b/tests/cip1559_test.py @@ -0,0 +1,213 @@ +from conflux.rpc import RpcClient +from test_framework.util import ( + assert_equal, +) +from decimal import Decimal +from typing import Literal + +from cfx_account import Account as CfxAccount +from cfx_account.signers.local import LocalAccount as CfxLocalAccount + +from test_framework.test_framework import ConfluxTestFramework +from test_framework.util import generate_blocks_for_base_fee_manipulation, assert_correct_fee_computation_for_core_tx + +CORE_BLOCK_GAS_TARGET = 270000 +BURNT_RATIO = 0.5 +MIN_NATIVE_BASE_PRICE = 10000 + +class CIP1559Test(ConfluxTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.conf_parameters["min_native_base_price"] = MIN_NATIVE_BASE_PRICE + self.conf_parameters["next_hardfork_transition_height"] = 1 + self.conf_parameters["next_hardfork_transition_number"] = 1 + + + def setup_network(self): + self.add_nodes(self.num_nodes) + self.start_node(0, ["--archive"]) + self.rpc = RpcClient(self.nodes[0]) + + # acct should have cfx + def change_base_fee(self, acct: CfxLocalAccount=None, block_count=10, tx_per_block=4, gas_per_tx=13500000): + if acct is None: + acct = self.init_acct_with_cfx() + generate_blocks_for_base_fee_manipulation(self.rpc, acct, block_count, tx_per_block, gas_per_tx) + + + def test_block_base_fee_change(self, acct: CfxLocalAccount, epoch_to_test:int, tx_per_block=4, gas_per_tx=13500000): + starting_epoch = self.rpc.epoch_number() + self.change_base_fee(acct, epoch_to_test, tx_per_block, gas_per_tx) + expected_base_fee_change_delta_rate = self.get_expected_base_fee_change_delta_rate(tx_per_block * gas_per_tx, 27000000) + + for i in range(starting_epoch+1, self.rpc.epoch_number()): + expected_current_base_fee = self.rpc.base_fee_per_gas(i-1) + int(self.rpc.base_fee_per_gas(i-1) * expected_base_fee_change_delta_rate) + assert_equal(self.rpc.base_fee_per_gas(i), expected_current_base_fee) + + + def get_expected_base_fee_change_delta_rate(self, sum_tx_gas_limit: int, block_target_gas_limit: int = None) -> Decimal: + if block_target_gas_limit is None: + block_target_gas_limit = CORE_BLOCK_GAS_TARGET + BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 + return ((sum_tx_gas_limit-block_target_gas_limit) / Decimal(block_target_gas_limit)) / BASE_FEE_MAX_CHANGE_DENOMINATOR + + # default to 1000 CFX + def init_acct_with_cfx(self, drip: int=10**21) -> CfxLocalAccount: + self.rpc.send_tx( + self.rpc.new_tx( + receiver=(acct:=CfxAccount.create()).address, + value=drip, + gas_price=max(self.rpc.base_fee_per_gas()*2,MIN_NATIVE_BASE_PRICE) # avoid genisis zero gas price + ), + True, + ) + return acct + + def get_gas_charged(self, tx_hash: str) -> int: + gas_limit = int(self.rpc.get_tx(tx_hash)["gas"], 16) + gas_used = int(self.rpc.get_transaction_receipt(tx_hash)["gasUsed"], 16) + return max(int(3/4*gas_limit), gas_used) + + + def test_balance_change(self, acct: CfxLocalAccount): + acct_balance = self.rpc.get_balance(acct.address) + h = self.rpc.send_tx( + self.rpc.new_typed_tx( + priv_key=acct.key.hex(), + receiver=CfxAccount.create().address, + max_fee_per_gas=self.rpc.base_fee_per_gas(), + max_priority_fee_per_gas=self.rpc.base_fee_per_gas(), + value=100, + ), + wait_for_receipt=True, + ) + receipt = self.rpc.get_transaction_receipt(h) + acct_new_balance = self.rpc.get_balance(acct.address) + assert_equal(acct_new_balance, acct_balance - int(receipt["gasFee"], 16) - 100) + + # this tests the case for pivot blocks + # as for non-pivot blocks, the tests are in ./cip137_test.py + def test_max_fee_not_enough_for_current_base_fee(self): + self.change_base_fee(block_count=10) + initial_base_fee = self.rpc.base_fee_per_gas() + + self.log.info(f"initla base fee: {initial_base_fee}") + + # 112.5% ^ 10 + self.change_base_fee(block_count=10) + self.log.info(f"increase base fee by 112.5% ^ 10") + self.log.info(f"new base fee: {self.rpc.base_fee_per_gas()}") + assert self.rpc.base_fee_per_gas() > initial_base_fee + self.log.info(f"sending new transaction with max_fee_per_gas: {initial_base_fee}") + # as the transaction's max fee per gas is not enough for current base fee, + # the transaction will become pending until the base fee drops + # we will observe the base fee of the block the transaction is in + h = self.rpc.send_tx( + self.rpc.new_typed_tx( + receiver=CfxAccount.create().address, + max_fee_per_gas=initial_base_fee, + ), + wait_for_receipt=True, + ) + + tx_base_fee = self.rpc.base_fee_per_gas(self.rpc.get_transaction_receipt(h)["epochNumber"]) + self.log.info(f"epoch base fee for transaction accepted: {tx_base_fee}") + assert tx_base_fee <= initial_base_fee + + def test_type_2_tx_fees(self): + + assert_correct_fee_computation_for_core_tx(self.rpc, self.rpc.send_tx( + self.rpc.new_typed_tx( + receiver=CfxAccount.create().address, + max_fee_per_gas=self.rpc.base_fee_per_gas(), + max_priority_fee_per_gas=self.rpc.base_fee_per_gas(), + ), + wait_for_receipt=True, + )) + assert_correct_fee_computation_for_core_tx(self.rpc, self.rpc.send_tx( + self.rpc.new_typed_tx( + receiver=CfxAccount.create().address, + max_fee_per_gas=self.rpc.base_fee_per_gas(), + max_priority_fee_per_gas=0, + ), + wait_for_receipt=True, + )) + self.test_balance_change(self.init_acct_with_cfx()) + + def test_balance_not_enough_for_base_fee(self): + # ensuring acct does not have enough balance to pay for base fee + initial_value = 21000*(MIN_NATIVE_BASE_PRICE-1) + acct = self.init_acct_with_cfx(initial_value) + block = self.rpc.generate_custom_block(parent_hash=self.rpc.block_by_epoch("latest_mined")["hash"], referee=[], txs=[ + self.rpc.new_typed_tx(value=0, gas=21000, priv_key=acct.key.hex()) + ]) + self.rpc.generate_blocks(20, 5) + # self. + # h = self.rpc.send_tx( + # self.rpc.new_typed_tx( + # priv_key=acct.key.hex(), + # max_fee_per_gas=self.rpc.base_fee_per_gas(), + # max_priority_fee_per_gas=self.rpc.base_fee_per_gas(), + # value=0, + # ), + # wait_for_receipt=True, + # ) + tx_data = self.rpc.block_by_hash(block, True)["transactions"][0] + tx_receipt = self.rpc.get_transaction_receipt(tx_data["hash"]) + gas_fee = int(tx_receipt["gasFee"],16) + assert_equal(gas_fee, initial_value) + assert_equal(tx_data["status"],"0x1") + # account balance is all consumed + assert_equal(self.rpc.get_balance(acct.address),0) + + # two cases to test based on balance enough for max priority fee per gas + # maxPriorityFeePerGas = maxFeePerGas <- will fail because balance is not enough for effective_gas_price * gas_charged + # maxPriorityFeePerGas = 0 <- succeed + def test_balance_enough_for_base_fee_but_not_for_max_fee_per_gas(self, priority_fee_setting: Literal["MAX", "ZERO"]): + # ensuring acct does not have enough balance to pay for base fee + self.log.info(f"current base fee: {self.rpc.base_fee_per_gas()}") + assert_equal(self.rpc.base_fee_per_gas(), MIN_NATIVE_BASE_PRICE) + # allow extra 1 priority fee + initial_value = 21000*(MIN_NATIVE_BASE_PRICE+1) + acct = self.init_acct_with_cfx(initial_value) + max_fee_per_gas = MIN_NATIVE_BASE_PRICE+2 + max_priority_fee: int + if priority_fee_setting == "MAX": + max_priority_fee = max_fee_per_gas + elif priority_fee_setting == "ZERO": + max_priority_fee = 0 + block = self.rpc.generate_custom_block(parent_hash=self.rpc.block_by_epoch("latest_mined")["hash"], referee=[], txs=[ + self.rpc.new_typed_tx(value=0, gas=21000, priv_key=acct.key.hex(), max_fee_per_gas=max_fee_per_gas, max_priority_fee_per_gas=max_priority_fee) + ]) + self.rpc.generate_blocks(20, 5) + + tx_data = self.rpc.block_by_hash(block, True)["transactions"][0] + assert_correct_fee_computation_for_core_tx(self.rpc, tx_data["hash"], BURNT_RATIO) + + if priority_fee_setting == "MAX": + # extra test to assert gas fee equal to all of the balance + tx_receipt = self.rpc.get_transaction_receipt(tx_data["hash"]) + gas_fee = int(tx_receipt["gasFee"],16) + assert_equal(gas_fee, initial_value) + + + def run_test(self): + self.rpc.generate_blocks(5) + + # test fee increasing + self.test_block_base_fee_change(self.init_acct_with_cfx(), 20, 4, 13500000) + self.test_block_base_fee_change(self.init_acct_with_cfx(), 20, 6, 8000000) + self.test_block_base_fee_change(self.init_acct_with_cfx(), 20, 3, 13500000) + # note: as min base fee is provided, we use less epochs + self.test_block_base_fee_change(self.init_acct_with_cfx(), 10, 1, 13500000) + self.test_block_base_fee_change(self.init_acct_with_cfx(), 10, 2, 10000000) + + self.test_type_2_tx_fees() + self.test_max_fee_not_enough_for_current_base_fee() + self.test_balance_not_enough_for_base_fee() + self.test_balance_enough_for_base_fee_but_not_for_max_fee_per_gas("ZERO") + self.test_balance_enough_for_base_fee_but_not_for_max_fee_per_gas("MAX") + + +if __name__ == "__main__": + CIP1559Test().main() diff --git a/tests/conflux/rpc.py b/tests/conflux/rpc.py index 66efb76676..6f66faa6e0 100644 --- a/tests/conflux/rpc.py +++ b/tests/conflux/rpc.py @@ -1,13 +1,15 @@ import os import random -from typing import Optional, Union +from typing import cast, Optional, Union, TypedDict, Any from web3 import Web3 import eth_utils +from cfx_account import Account as CfxAccount +from eth_account.datastructures import SignedTransaction import rlp import json -from .address import hex_to_b32_address, b32_address_to_hex +from .address import hex_to_b32_address, b32_address_to_hex, DEFAULT_PY_TEST_CHAIN_ID from .config import DEFAULT_PY_TEST_CHAIN_ID, default_config from .transactions import CONTRACT_DEFAULT_GAS, Transaction, UnsignedTransaction from .filter import Filter @@ -25,6 +27,7 @@ assert_equal, wait_until, checktx, get_contract_instance ) +from test_framework.test_node import TestNode file_dir = os.path.dirname(os.path.realpath(__file__)) REQUEST_BASE = { @@ -34,6 +37,11 @@ "to": b'', } +class CfxFeeHistoryResponse(TypedDict): + baseFeePerGas: list[int] + gasUsedRatio: list[float] + reward: list[list[str]] # does not convert it currently + def convert_b32_address_field_to_hex(original_dict: dict, field_name: str): if original_dict is not None and field_name in original_dict and original_dict[field_name] not in [None, "null"]: @@ -41,8 +49,8 @@ def convert_b32_address_field_to_hex(original_dict: dict, field_name: str): class RpcClient: - def __init__(self, node=None, auto_restart=False, log=None): - self.node = node + def __init__(self, node: Optional[TestNode]=None, auto_restart=False, log=None): + self.node: TestNode = node # type: ignore self.auto_restart = auto_restart self.log = log @@ -129,13 +137,22 @@ def generate_block_with_parent(self, parent_hash: str, referee: list = None, num assert_is_hash_string(block_hash) return block_hash - def generate_custom_block(self, parent_hash: str, referee: list, txs: list) -> str: + def generate_custom_block(self, parent_hash: str, referee: list, txs: list[Union[Transaction, SignedTransaction]]) -> str: assert_is_hash_string(parent_hash) for r in referee: assert_is_hash_string(r) - encoded_txs = eth_utils.encode_hex(rlp.encode(txs)) + raw_txs = [] + for tx in txs: + if isinstance(tx, SignedTransaction): + raw_txs.append(tx.rawTransaction) + elif isinstance(tx, Transaction): + raw_txs.append(rlp.encode(tx)) + else: + raw_txs.append(rlp.encode(tx)) + + encoded_txs = eth_utils.encode_hex(rlp.encode(raw_txs)) block_hash = self.node.test_generatecustomblock(parent_hash, referee, encoded_txs) assert_is_hash_string(block_hash) @@ -188,6 +205,9 @@ def get_code(self, address: str, epoch: Union[str, dict] = None) -> str: def gas_price(self) -> int: return int(self.node.cfx_gasPrice(), 0) + def base_fee_per_gas(self, epoch: Union[int,str] = "latest_mined"): + return int(self.block_by_epoch(epoch).get("baseFeePerGas", "0x0"), 16) + def get_block_reward_info(self, epoch: str): reward = self.node.cfx_getBlockRewardInfo(epoch) convert_b32_address_field_to_hex(reward, "author") @@ -296,8 +316,12 @@ def send_raw_tx(self, raw_tx: str, wait_for_catchup=True) -> str: def clear_tx_pool(self): self.node.txpool_clear() - def send_tx(self, tx: Transaction, wait_for_receipt=False, wait_for_catchup=True) -> str: - encoded = eth_utils.encode_hex(rlp.encode(tx)) + # a temporary patch for transaction compatibity + def send_tx(self, tx: Union[Transaction, SignedTransaction], wait_for_receipt=False, wait_for_catchup=True) -> str: + if isinstance(tx, SignedTransaction): + encoded = cast(str, tx.rawTransaction.hex()) + else: + encoded = eth_utils.encode_hex(rlp.encode(tx)) tx_hash = self.send_raw_tx(encoded, wait_for_catchup=wait_for_catchup) if wait_for_receipt: @@ -360,7 +384,7 @@ def get_tx(self, tx_hash: str) -> dict: convert_b32_address_field_to_hex(tx, "contractCreated") return tx - def new_tx(self, sender=None, receiver=None, nonce=None, gas_price=1, gas=21000, value=100, data=b'', sign=True, + def new_tx(self, *, sender=None, receiver=None, nonce=None, gas_price=1, gas=21000, value=100, data=b'', sign=True, priv_key=None, storage_limit=None, epoch_height=None, chain_id=DEFAULT_PY_TEST_CHAIN_ID): if priv_key is None: priv_key = default_config["GENESIS_PRI_KEY"] @@ -386,6 +410,49 @@ def new_tx(self, sender=None, receiver=None, nonce=None, gas_price=1, gas=21000, return tx.sign(priv_key) else: return tx + + def new_typed_tx(self, *, type_=2, receiver=None, nonce=None, max_fee_per_gas=None,max_priority_fee_per_gas=0, access_list=[], gas=21000, value=100, data=b'', + priv_key=None, storage_limit=0, epoch_height=None, chain_id=DEFAULT_PY_TEST_CHAIN_ID + ) -> SignedTransaction: + + if priv_key: + acct = CfxAccount.from_key(priv_key, DEFAULT_PY_TEST_CHAIN_ID) + else: + acct = CfxAccount.from_key(default_config["GENESIS_PRI_KEY"], DEFAULT_PY_TEST_CHAIN_ID) + if receiver is None: + receiver = self.COINBASE_ADDR + tx = {} + tx["type"] = type_ + tx["gas"] = gas + tx["storageLimit"] = storage_limit + tx["value"] = value + tx["data"] = data + tx["maxPriorityFeePerGas"] = max_priority_fee_per_gas + tx["chainId"] = chain_id + tx["to"] = receiver + + if nonce is None: + nonce = self.get_nonce(acct.hex_address) + tx["nonce"] = nonce + + if access_list != []: + def format_access_list(a_list): + rtn = [] + for item in a_list: + rtn.append({"address": item['address'], "storageKeys": item['storage_keys']}) + + access_list = format_access_list(access_list) + tx["accessList"] = access_list + + if epoch_height is None: + epoch_height = self.epoch_number() + tx["epochHeight"] = epoch_height + + # ensuring transaction can be sent + if max_fee_per_gas is None: + max_fee_per_gas = self.base_fee_per_gas('latest_mined') + 1 + tx["maxFeePerGas"] = max_fee_per_gas + return acct.sign_transaction(tx) def new_contract_tx(self, receiver: Optional[str], data_hex: str = None, sender=None, priv_key=None, nonce=None, gas_price=1, @@ -450,7 +517,7 @@ def disconnect_peer(self, node_id: str, node_op: str = None) -> int: def chain(self) -> list: return self.node.cfx_getChain() - def get_transaction_receipt(self, tx_hash: str) -> dict: + def get_transaction_receipt(self, tx_hash: str) -> dict[str, Any]: assert_is_hash_string(tx_hash) r = self.node.cfx_getTransactionReceipt(tx_hash) if r is None: @@ -580,6 +647,18 @@ def get_transaction_trace(self, tx_hash: str): def filter_trace(self, filter: dict): return self.node.trace_filter(filter) + + def fee_history(self, epoch_count: int, last_epoch: Union[int, str], reward_percentiles: Optional[list[float]]=None) -> CfxFeeHistoryResponse: + if reward_percentiles is None: + reward_percentiles = [50] + if isinstance(last_epoch, int): + last_epoch = hex(last_epoch) + rtn = self.node.cfx_feeHistory(hex(epoch_count), last_epoch, reward_percentiles) + rtn[ + 'baseFeePerGas' + ] = [ int(v, 16) for v in rtn['baseFeePerGas'] ] + return rtn + def wait_for_pos_register(self, priv_key=None, stake_value=2_000_000, voting_power=None, legacy=True, should_fail=False): if priv_key is None: diff --git a/tests/test_framework/simple_rpc_proxy.py b/tests/test_framework/simple_rpc_proxy.py index 6105f26032..71cd2d7bb1 100644 --- a/tests/test_framework/simple_rpc_proxy.py +++ b/tests/test_framework/simple_rpc_proxy.py @@ -1,4 +1,5 @@ import time +from typing import Any import jsonrpcclient.client from jsonrpcclient.exceptions import ReceivedErrorResponseError @@ -25,7 +26,7 @@ def __init__(self, client, method, timeout, node): self.timeout = timeout self.node = node - def __call__(self, *args, **argsn): + def __call__(self, *args, **argsn) -> Any: if argsn: raise ValueError('json rpc 2 only supports array arguments') from jsonrpcclient.requests import Request diff --git a/tests/test_framework/test_framework.py b/tests/test_framework/test_framework.py index 2abc46690c..b66c0957fa 100644 --- a/tests/test_framework/test_framework.py +++ b/tests/test_framework/test_framework.py @@ -73,7 +73,7 @@ class ConfluxTestFramework: def __init__(self): """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" self.setup_clean_chain = True - self.nodes = [] + self.nodes: list[TestNode] = [] self.network_thread = None self.mocktime = 0 self.rpc_timewait = CONFLUX_RPC_WAIT_TIMEOUT diff --git a/tests/test_framework/test_node.py b/tests/test_framework/test_node.py index 05a240739d..2cbac1ce5b 100644 --- a/tests/test_framework/test_node.py +++ b/tests/test_framework/test_node.py @@ -74,7 +74,7 @@ def __init__(self, index, datadir, rpchost, confluxd, rpc_timeout=None, remote=F self.running = False self.process = None self.rpc_connected = False - self.rpc = None + self.rpc: SimpleRpcProxy = None # type: ignore self.ethrpc = None self.ethrpc_connected = False self.log = logging.getLogger('TestFramework.node%d' % index) diff --git a/tests/test_framework/util.py b/tests/test_framework/util.py index c8b8aac316..e2ac3329d9 100644 --- a/tests/test_framework/util.py +++ b/tests/test_framework/util.py @@ -10,15 +10,18 @@ import re from subprocess import CalledProcessError, check_output import time -from typing import Optional, Callable, List, TYPE_CHECKING, cast +from typing import Optional, Callable, List, TYPE_CHECKING, cast, Tuple, Union import socket import threading import jsonrpcclient.exceptions import solcx import web3 +from cfx_account import Account as CfxAccount +from cfx_account.signers.local import LocalAccount as CfxLocalAccount from sys import platform import yaml import shutil +import math from test_framework.simple_rpc_proxy import SimpleRpcProxy from . import coverage @@ -806,3 +809,103 @@ def test_rpc_call_with_block_object(client: "RpcClient", txs: List, rpc_call: Ca assert(expected_result_lambda(result1)) assert_equal(result2, result1) + +# acct should have cfx +# create a chain of blocks with specified transfer tx with specified num and gas +# return the last block's hash and acct nonce +def generate_blocks_for_base_fee_manipulation(rpc: "RpcClient", acct: Union[CfxLocalAccount, str], block_count=10, tx_per_block=4, gas_per_tx=13500000,initial_parent_hash:str = None) -> Tuple[str, int]: + if isinstance(acct, str): + acct = CfxAccount.from_key(acct) + starting_nonce: int = rpc.get_nonce(acct.hex_address) + + if initial_parent_hash is None: + initial_parent_hash = cast(str, rpc.block_by_epoch("latest_mined")["hash"]) + + block_pointer = initial_parent_hash + for block_count in range(block_count): + block_pointer, starting_nonce = generate_single_block_for_base_fee_manipulation(rpc, acct, tx_per_block=tx_per_block, gas_per_tx=gas_per_tx,parent_hash=block_pointer, starting_nonce=starting_nonce) + + return block_pointer, starting_nonce + block_count * tx_per_block + +def generate_single_block_for_base_fee_manipulation(rpc: "RpcClient", acct: CfxLocalAccount, referee:list[str] =[], tx_per_block=4, gas_per_tx=13500000,parent_hash:str = None, starting_nonce: int = None) -> Tuple[str, int]: + if starting_nonce is None: + starting_nonce = cast(int, rpc.get_nonce(acct.hex_address)) + + if parent_hash is None: + parent_hash = cast(str, rpc.block_by_epoch("latest_mined")["hash"]) + + new_block = rpc.generate_custom_block( + txs=[ + rpc.new_tx( + priv_key=acct.key, + receiver=acct.address, + gas=gas_per_tx, + nonce=starting_nonce + i , + gas_price=rpc.base_fee_per_gas()*2 # give enough gas price to make the tx valid + ) + for i in range(tx_per_block) + ], + parent_hash=parent_hash, + referee=referee, + ) + return new_block, starting_nonce + tx_per_block + +# for transactions in either pivot/non-pivot block +# checks priority fee is calculated as expeted +def assert_correct_fee_computation_for_core_tx(rpc: "RpcClient", tx_hash: str, burnt_ratio=0.5): + def get_gas_charged(rpc: "RpcClient", tx_hash: str) -> int: + gas_limit = int(rpc.get_tx(tx_hash)["gas"], 16) + gas_used = int(rpc.get_transaction_receipt(tx_hash)["gasUsed"], 16) + return max(int(3/4*gas_limit), gas_used) + + receipt = rpc.get_transaction_receipt(tx_hash) + # The transaction is not executed + if receipt is None: + return + + tx_data = rpc.get_tx(tx_hash) + tx_type = int(tx_data["type"], 16) + if tx_type == 2: + # original tx fields + max_fee_per_gas = int(tx_data["maxFeePerGas"], 16) + max_priority_fee_per_gas = int(tx_data["maxPriorityFeePerGas"], 16) + else: + max_fee_per_gas = int(tx_data["gasPrice"], 16) + max_priority_fee_per_gas = int(tx_data["gasPrice"], 16) + + effective_gas_price = int(receipt["effectiveGasPrice"], 16) + transaction_epoch = int(receipt["epochNumber"],16) + is_in_pivot_block = rpc.block_by_epoch(transaction_epoch)["hash"] == receipt["blockHash"] + base_fee_per_gas = rpc.base_fee_per_gas(transaction_epoch) + burnt_fee_per_gas = math.ceil(base_fee_per_gas * burnt_ratio) + gas_fee = int(receipt["gasFee"], 16) + burnt_gas_fee = int(receipt["burntGasFee"], 16) + gas_charged = get_gas_charged(rpc, tx_hash) + + # check gas fee computation + # print("effective gas price: ", effective_gas_price) + # print("gas charged: ", get_gas_charged(rpc, tx_hash)) + # print("gas fee", gas_fee) + + # check gas fee and burnt gas fee computation + if receipt["outcomeStatus"] == "0x1": # tx fails becuase of not enough cash + assert "NotEnoughCash" in receipt["txExecErrorMsg"] + # all gas is charged + assert_equal(rpc.get_balance(tx_data["from"], receipt["epochNumber"]), 0) + # gas fee less than effective gas price + assert gas_fee < effective_gas_price*gas_charged + else: + assert_equal(gas_fee, effective_gas_price*gas_charged) + # check burnt fee computation + assert_equal(burnt_gas_fee, burnt_fee_per_gas*gas_charged) + + # if max_fee_per_gas >= base_fee_per_gas, it shall follow the computation, regardless of transaction in pivot block or not + if max_fee_per_gas >= base_fee_per_gas: + priority_fee_per_gas = effective_gas_price - base_fee_per_gas + # check priority fee computation + assert_equal(priority_fee_per_gas, min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas)) + else: + # max fee per gas should be greater than burnt fee per gas + assert is_in_pivot_block == False, "Transaction should be in non-pivot block" + assert max_fee_per_gas >= burnt_fee_per_gas + diff --git a/tests/tx_consistency_test.py b/tests/tx_consistency_test.py index f3eead320b..a5db4ce784 100755 --- a/tests/tx_consistency_test.py +++ b/tests/tx_consistency_test.py @@ -148,7 +148,7 @@ def sample_node_indices(self): # randomly select N nodes to send tx. def send_tx(self, sender: Account, receiver: Account): client = RpcClient(self.nodes[0]) - tx = client.new_tx(sender.address, receiver.address, sender.nonce, value=9000, priv_key=sender.priv_key) + tx = client.new_tx(sender=sender.address, receiver=receiver.address, nonce=sender.nonce, value=9000, priv_key=sender.priv_key) def ensure_send_tx(node, tx): tx_hash = RpcClient(node).send_tx(tx)