From 90517f199301e456618afe2d70b6297ef5e9792f Mon Sep 17 00:00:00 2001 From: keroro520 Date: Wed, 11 Sep 2019 17:10:50 +0800 Subject: [PATCH 1/7] chore: Export all test cases in specs --- test/src/specs/alert/mod.rs | 2 +- test/src/specs/indexer/mod.rs | 4 ++-- test/src/specs/mining/fee.rs | 1 + test/src/specs/mining/mod.rs | 8 +++++--- test/src/specs/p2p/mod.rs | 8 ++++---- test/src/specs/relay/mod.rs | 7 ++----- test/src/specs/sync/mod.rs | 10 +++++----- test/src/specs/tx_pool/mod.rs | 18 +++++++++--------- 8 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 test/src/specs/mining/fee.rs diff --git a/test/src/specs/alert/mod.rs b/test/src/specs/alert/mod.rs index ce6d846d3f..d40d066ce5 100644 --- a/test/src/specs/alert/mod.rs +++ b/test/src/specs/alert/mod.rs @@ -1,6 +1,6 @@ mod alert_propagation; -pub use alert_propagation::AlertPropagation; +pub use alert_propagation::*; use ckb_crypto::secp::Privkey; use ckb_jsonrpc_types::JsonBytes; diff --git a/test/src/specs/indexer/mod.rs b/test/src/specs/indexer/mod.rs index f0808b56a8..3948234fcc 100644 --- a/test/src/specs/indexer/mod.rs +++ b/test/src/specs/indexer/mod.rs @@ -1,5 +1,5 @@ mod basic; mod genesis_issued_cells; -pub use basic::IndexerBasic; -pub use genesis_issued_cells::GenesisIssuedCells; +pub use basic::*; +pub use genesis_issued_cells::*; diff --git a/test/src/specs/mining/fee.rs b/test/src/specs/mining/fee.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test/src/specs/mining/fee.rs @@ -0,0 +1 @@ + diff --git a/test/src/specs/mining/mod.rs b/test/src/specs/mining/mod.rs index 14ca8ed414..dc97ba6548 100644 --- a/test/src/specs/mining/mod.rs +++ b/test/src/specs/mining/mod.rs @@ -1,9 +1,11 @@ mod basic; mod bootstrap; +mod fee; mod size_limit; mod uncle; -pub use basic::MiningBasic; -pub use bootstrap::BootstrapCellbase; -pub use size_limit::TemplateSizeLimit; +pub use basic::*; +pub use bootstrap::*; +pub use fee::*; +pub use size_limit::*; pub use uncle::*; diff --git a/test/src/specs/p2p/mod.rs b/test/src/specs/p2p/mod.rs index bb8076515d..30d3de4711 100644 --- a/test/src/specs/p2p/mod.rs +++ b/test/src/specs/p2p/mod.rs @@ -3,7 +3,7 @@ mod discovery; mod malformed_message; mod whitelist; -pub use disconnect::Disconnect; -pub use discovery::Discovery; -pub use malformed_message::{MalformedMessage, MalformedMessageWithWhitelist}; -pub use whitelist::WhitelistOnSessionLimit; +pub use disconnect::*; +pub use discovery::*; +pub use malformed_message::*; +pub use whitelist::*; diff --git a/test/src/specs/relay/mod.rs b/test/src/specs/relay/mod.rs index ae173b700a..2f47d792f8 100644 --- a/test/src/specs/relay/mod.rs +++ b/test/src/specs/relay/mod.rs @@ -2,9 +2,6 @@ mod block_relay; mod compact_block; mod transaction_relay; -pub use block_relay::BlockRelayBasic; +pub use block_relay::*; pub use compact_block::*; -pub use transaction_relay::{ - RelayInvalidTransaction, TransactionRelayBasic, TransactionRelayMultiple, - TransactionRelayTimeout, -}; +pub use transaction_relay::*; diff --git a/test/src/specs/sync/mod.rs b/test/src/specs/sync/mod.rs index c7c5df6e7e..cfd40a60d3 100644 --- a/test/src/specs/sync/mod.rs +++ b/test/src/specs/sync/mod.rs @@ -9,8 +9,8 @@ mod utils; pub use block_sync::*; pub use chain_forks::*; -pub use get_blocks::GetBlocksTimeout; -pub use ibd_process::{IBDProcess, IBDProcessWithWhiteList}; -pub use invalid_block::{ChainContainsInvalidBlock, ForkContainsInvalidBlock}; -pub use invalid_locator_size::InvalidLocatorSize; -pub use sync_timeout::SyncTimeout; +pub use get_blocks::*; +pub use ibd_process::*; +pub use invalid_block::*; +pub use invalid_locator_size::*; +pub use sync_timeout::*; diff --git a/test/src/specs/tx_pool/mod.rs b/test/src/specs/tx_pool/mod.rs index 4b47f3bc2e..dd47400705 100644 --- a/test/src/specs/tx_pool/mod.rs +++ b/test/src/specs/tx_pool/mod.rs @@ -8,12 +8,12 @@ mod reference_header_maturity; mod send_secp_tx; mod valid_since; -pub use cellbase_maturity::CellbaseMaturity; -pub use depend_tx_in_same_block::DepentTxInSameBlock; -pub use different_txs_with_same_input::DifferentTxsWithSameInput; -pub use limit::{CyclesLimit, SizeLimit}; -pub use pool_reconcile::PoolReconcile; -pub use pool_resurrect::PoolResurrect; -pub use reference_header_maturity::ReferenceHeaderMaturity; -pub use send_secp_tx::{CheckTypical2In2OutTx, SendSecpTxUseDepGroup}; -pub use valid_since::ValidSince; +pub use cellbase_maturity::*; +pub use depend_tx_in_same_block::*; +pub use different_txs_with_same_input::*; +pub use limit::*; +pub use pool_reconcile::*; +pub use pool_resurrect::*; +pub use reference_header_maturity::*; +pub use send_secp_tx::*; +pub use valid_since::*; From fd0b59e82109151d09d72b3a543ef21da81edeef Mon Sep 17 00:00:00 2001 From: keroro520 Date: Thu, 19 Sep 2019 15:33:18 +0800 Subject: [PATCH 2/7] feat(test): Add TXOSet to manage cells --- test/src/txo.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 test/src/txo.rs diff --git a/test/src/txo.rs b/test/src/txo.rs new file mode 100644 index 0000000000..04432f5ef1 --- /dev/null +++ b/test/src/txo.rs @@ -0,0 +1,211 @@ +use ckb_types::core::{Capacity, TransactionBuilder, TransactionView}; +use ckb_types::packed::{CellDep, CellInput, CellOutput, OutPoint, Script}; +use ckb_types::prelude::*; +use rand::{thread_rng, Rng}; +use std::collections::HashMap; + +const EXPLODE_LIMIT: usize = 2000; + +#[derive(Debug, Clone)] +pub struct TXO { + out_point: OutPoint, + output: CellOutput, +} + +#[derive(Default, Debug)] +pub struct TXOSet { + txos: HashMap, +} + +impl TXO { + pub fn new(out_point: OutPoint, output: CellOutput) -> Self { + Self { out_point, output } + } + + pub fn capacity(&self) -> u64 { + self.output.capacity().unpack() + } + + pub fn lock(&self) -> Script { + self.output.lock() + } + + pub fn to_input(&self) -> CellInput { + CellInput::new_builder() + .previous_output(self.out_point.clone()) + .build() + } + + /// Return a `CellOutput` with the equivalent capacity to the original TXO + pub fn to_equivalent_output(&self) -> CellOutput { + CellOutput::new_builder() + .lock(self.lock()) + .capacity(self.capacity().pack()) + .build() + } + + /// Return a `CellOutput` with the minimal capacity + pub fn to_minimal_output(&self) -> CellOutput { + CellOutput::new_builder() + .lock(self.lock()) + .build_exact_capacity(Capacity::zero()) + .unwrap() + } +} + +impl TXOSet { + pub fn new(txos: T) -> Self + where + T: IntoIterator, + { + Self { + txos: txos + .into_iter() + .map(|txo: TXO| (txo.out_point, txo.output)) + .collect(), + } + } + + pub fn len(&self) -> usize { + self.txos.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn total_capacity(&self) -> u64 { + self.iter().map(|txo| txo.capacity()).sum() + } + + pub fn get(&self, out_point: &OutPoint) -> Option { + self.txos + .get(out_point) + .map(|output| TXO::new(out_point.clone(), output.clone())) + } + + pub fn truncate(&mut self, len: usize) { + if self.txos.len() > len { + self.txos = self + .txos + .iter() + .take(len) + .map(|(out_point, output)| (out_point.clone(), output.clone())) + .collect(); + } + } + + pub fn extend(&mut self, other: T) + where + T: Into, + { + self.txos.extend(Into::::into(other).txos) + } + + /// Construct a transaction that explodes the large UTXOs into small UTXOs + /// + // NOTE: Limit the number of outputs, to avoid transaction size exceeding limit + // NOTE: Assume empty output_data for simplicity + pub fn boom(&self, cell_deps: C) -> TransactionView + where + C: IntoIterator, + { + let outputs = self.boom_to_minimals(); + let outputs_data: Vec<_> = outputs.iter().map(|_| Default::default()).collect(); + TransactionBuilder::default() + .cell_deps(cell_deps) + .inputs(self.iter().map(|txo| txo.to_input())) + .outputs(outputs) + .outputs_data(outputs_data) + .build() + } + + /// Construct transactions which convert the UTXO to another UTXO with equivalent capacity + // NOTE: Assume empty output_data for simplicity + pub fn bang(&self, cell_deps: C) -> Vec + where + C: IntoIterator, + { + let cell_deps: Vec<_> = cell_deps.into_iter().collect(); + self.iter() + .map(|txo| { + TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .input(txo.to_input()) + .output(txo.to_equivalent_output()) + .output_data(Default::default()) + .build() + }) + .collect() + } + + /// Construct transactions which convert the UTXO to another UTXO, given random fees + pub fn bang_random_fee(&self, cell_deps: C) -> Vec + where + C: IntoIterator, + { + let cell_deps: Vec<_> = cell_deps.into_iter().collect(); + let mut rng = thread_rng(); + self.iter() + .map(|txo| { + let maximal_capacity = txo.capacity(); + let minimal_capacity: u64 = txo.to_minimal_output().capacity().unpack(); + let actual_capacity = rng.gen_range(minimal_capacity, maximal_capacity + 1); + let output = txo + .to_equivalent_output() + .as_builder() + .capacity(actual_capacity.pack()) + .build(); + TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .input(txo.to_input()) + .output(output) + .output_data(Default::default()) + .build() + }) + .collect() + } + + fn boom_to_minimals(&self) -> Vec { + let mut input_capacity = self.total_capacity(); + let minimal = self.iter().next().unwrap().to_minimal_output(); + let minimal_capacity: u64 = minimal.capacity().unpack(); + let mut outputs = Vec::new(); + while outputs.len() < EXPLODE_LIMIT { + if input_capacity < 2 * minimal_capacity || outputs.len() == EXPLODE_LIMIT { + outputs.push(minimal.as_builder().capacity(input_capacity.pack()).build()); + break; + } else { + input_capacity -= minimal_capacity; + outputs.push(minimal.clone()); + } + } + outputs + } + + pub fn iter(&self) -> impl Iterator { + // NOTE: Cloning is wasteful but okay for testing, I think + self.txos + .clone() + .into_iter() + .map(|(out_point, output)| TXO::new(out_point, output)) + } +} + +impl From<&TransactionView> for TXOSet { + fn from(transaction: &TransactionView) -> Self { + let tx_hash = transaction.hash(); + let txos = transaction + .outputs() + .into_iter() + .enumerate() + .map(move |(i, output)| { + let out_point = OutPoint::new_builder() + .tx_hash(tx_hash.clone()) + .index(i.pack()) + .build(); + TXO::new(out_point, output) + }); + Self::new(txos) + } +} From d7b4db26db81c4d1a0baf18ee1afa500c5fe1ae3 Mon Sep 17 00:00:00 2001 From: keroro520 Date: Thu, 19 Sep 2019 15:34:01 +0800 Subject: [PATCH 3/7] feat(test): Provide generating live cells --- test/src/utils.rs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/src/utils.rs b/test/src/utils.rs index 5bf48fb13a..54ee914a47 100644 --- a/test/src/utils.rs +++ b/test/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{Net, Node}; +use crate::{Net, Node, TXOSet}; use ckb_jsonrpc_types::{BlockTemplate, TransactionWithStatus, TxStatus}; use ckb_types::{ bytes::Bytes, @@ -205,3 +205,41 @@ pub fn temp_path() -> String { tempdir.close().expect("close tempdir failed"); path } + +/// Generate new blocks and explode these cellbases into `n` live cells +pub fn generate_utxo_set(node: &Node, n: usize) -> TXOSet { + // Ensure all the cellbases will be used later are already mature. + let cellbase_maturity = node.consensus().cellbase_maturity(); + node.generate_blocks(cellbase_maturity as usize); + + // Explode these mature cellbases into multiple cells + let mut n_outputs = 0; + let mut txs = Vec::new(); + while n > n_outputs { + node.generate_block(); + let mature_number = node.get_tip_block_number() - cellbase_maturity; + let mature_block = node.get_block_by_number(mature_number); + let mature_cellbase = mature_block.transaction(0).unwrap(); + let mature_utxos: TXOSet = TXOSet::from(&mature_cellbase); + let tx = mature_utxos.boom(vec![node.always_success_cell_dep()]); + n_outputs += tx.outputs().len(); + txs.push(tx); + } + + // Ensure all the transactions were committed + txs.iter().for_each(|tx| { + node.submit_transaction(tx); + }); + while txs + .iter() + .any(|tx| !is_committed(&node.rpc_client().get_transaction(tx.hash()).unwrap())) + { + node.generate_blocks(node.consensus().finalization_delay_length() as usize); + } + + let mut utxos = TXOSet::default(); + txs.iter() + .for_each(|tx| utxos.extend(Into::::into(tx))); + utxos.truncate(n); + utxos +} From 0431614827a453149072a5d2f9539b251955c3c1 Mon Sep 17 00:00:00 2001 From: keroro520 Date: Thu, 19 Sep 2019 15:34:56 +0800 Subject: [PATCH 4/7] test: Add forked_transaction --- test/src/specs/sync/chain_forks.rs | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/src/specs/sync/chain_forks.rs b/test/src/specs/sync/chain_forks.rs index 3347df9b7d..29457a7758 100644 --- a/test/src/specs/sync/chain_forks.rs +++ b/test/src/specs/sync/chain_forks.rs @@ -1,6 +1,7 @@ use crate::utils::is_committed; use crate::{Net, Node, Spec, TestProtocol, DEFAULT_TX_PROPOSAL_WINDOW}; use ckb_app_config::CKBAppConfig; +use ckb_jsonrpc_types::{TransactionWithStatus, TxStatus}; use ckb_types::{ core::{capacity_bytes, BlockView, Capacity, TransactionView}, h256, @@ -611,6 +612,63 @@ impl Spec for ForksContainSameUncle { } } +pub struct ForkedTransaction; + +impl Spec for ForkedTransaction { + crate::name!("forked_transaction"); + + crate::setup!(num_nodes: 2, connect_all: false); + + // Case: Check TxStatus of transaction on main-fork, verified-fork and unverified-fork + fn run(&self, net: &mut Net) { + let node0 = &net.nodes[0]; + let node1 = &net.nodes[1]; + let finalization_delay_length = node0.consensus().finalization_delay_length(); + + net.exit_ibd_mode(); + let fixed_point = node0.get_tip_block_number(); + let tx = node1.new_transaction_spend_tip_cellbase(); + + // node0 doesn't have `tx` => TxStatus: None + // node1 have `tx` on main-fork => TxStatus: Some(Committed) + { + node0.generate_blocks(1 + 2 * finalization_delay_length as usize); + node1.submit_transaction(&tx); + node1.generate_blocks(2 * finalization_delay_length as usize); + + let tx_status = node0.rpc_client().get_transaction(tx.hash()); + assert!(tx_status.is_none(), "node0 maintains tx in unverified fork"); + let tx_status = node1.rpc_client().get_transaction(tx.hash()).unwrap(); + is_committed(&tx_status); + } + + // node0 have `tx` on unverified-fork => TxStatus: None + // node1 have `tx` on verified-fork => TxStatus: Some(Pending) + { + (fixed_point..=node1.get_tip_block_number()).for_each(|number| { + let block = node1.get_block_by_number(number); + node0.submit_block(&block.data()); + }); + (fixed_point..=node0.get_tip_block_number()).for_each(|number| { + let block = node0.get_block_by_number(number); + node1.submit_block(&block.data()); + }); + + let tx_status = node0.rpc_client().get_transaction(tx.hash()); + assert!(tx_status.is_none(), "node0 maintains tx in unverified fork"); + let is_pending = |tx_status: &TransactionWithStatus| { + let pending_status = TxStatus::pending(); + tx_status.tx_status.status == pending_status.status + }; + let tx_status = node1.rpc_client().get_transaction(tx.hash()).unwrap(); + assert!( + is_pending(&tx_status), + "node1 maintains tx in verified fork" + ); + } + } +} + fn modify_block_transaction( block: BlockView, transaction_index: usize, From 289b4ce9488f8313b41309df5296f20c45d03a62 Mon Sep 17 00:00:00 2001 From: keroro520 Date: Thu, 19 Sep 2019 15:35:41 +0800 Subject: [PATCH 5/7] test: Add cases about proposed/committed rewards --- test/src/lib.rs | 2 + test/src/main.rs | 7 + test/src/node.rs | 20 +- test/src/rpc.rs | 12 +- test/src/specs/mining/fee.rs | 354 +++++++++++++++++++++++++++++++++++ 5 files changed, 385 insertions(+), 10 deletions(-) diff --git a/test/src/lib.rs b/test/src/lib.rs index 1449c3a302..df578f7c7e 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -2,6 +2,7 @@ mod net; mod node; mod rpc; pub mod specs; +mod txo; mod utils; use ckb_types::core::BlockNumber; @@ -9,6 +10,7 @@ use ckb_types::core::BlockNumber; pub use net::Net; pub use node::Node; pub use specs::{Setup, Spec, TestProtocol}; +pub use txo::{TXOSet, TXO}; // ckb doesn't support tx proposal window configuration, use a hardcoded value for integration test. pub const DEFAULT_TX_PROPOSAL_WINDOW: (BlockNumber, BlockNumber) = (2, 10); diff --git a/test/src/main.rs b/test/src/main.rs index 47ed9dad0d..745d6ec03f 100644 --- a/test/src/main.rs +++ b/test/src/main.rs @@ -15,6 +15,7 @@ use std::time::Instant; #[allow(clippy::cognitive_complexity)] fn main() { + env::set_var("RUST_BACKTRACE", "full"); let _ = { let filter = ::std::env::var("CKB_LOG").unwrap_or_else(|_| "info".to_string()); env_logger::builder().parse_filters(&filter).try_init() @@ -279,6 +280,12 @@ fn all_specs() -> SpecMap { Box::new(UncleInheritFromForkBlock), Box::new(UncleInheritFromForkUncle), Box::new(PackUnclesIntoEpochStarting), + Box::new(FeeOfTransaction), + Box::new(FeeOfMaxBlockProposalsLimit), + Box::new(FeeOfMultipleMaxBlockProposalsLimit), + Box::new(ProposeButNotCommit), + Box::new(ProposeDuplicated), + Box::new(ForkedTransaction), ]; specs.into_iter().map(|spec| (spec.name(), spec)).collect() } diff --git a/test/src/node.rs b/test/src/node.rs index 77fd93cbd2..9237ec5e41 100644 --- a/test/src/node.rs +++ b/test/src/node.rs @@ -355,18 +355,11 @@ impl Node { since: u64, capacity: Capacity, ) -> TransactionView { - let always_success_out_point = OutPoint::new( - self.genesis_cellbase_hash.clone(), - SYSTEM_CELL_ALWAYS_SUCCESS_INDEX, - ); + let always_success_cell_dep = self.always_success_cell_dep(); let always_success_script = self.always_success_script(); core::TransactionBuilder::default() - .cell_dep( - CellDep::new_builder() - .out_point(always_success_out_point) - .build(), - ) + .cell_dep(always_success_cell_dep) .output( CellOutputBuilder::default() .capacity(capacity.pack()) @@ -385,6 +378,15 @@ impl Node { .build() } + pub fn always_success_cell_dep(&self) -> CellDep { + CellDep::new_builder() + .out_point(OutPoint::new( + self.genesis_cellbase_hash.clone(), + SYSTEM_CELL_ALWAYS_SUCCESS_INDEX, + )) + .build() + } + fn prepare_chain_spec( &mut self, modify_chain_spec: Box ()>, diff --git a/test/src/rpc.rs b/test/src/rpc.rs index f4fc9dac66..8877adb71e 100644 --- a/test/src/rpc.rs +++ b/test/src/rpc.rs @@ -1,5 +1,5 @@ use ckb_jsonrpc_types::{ - Alert, BannedAddr, Block, BlockNumber, BlockTemplate, BlockView, Capacity, + Alert, BannedAddr, Block, BlockNumber, BlockReward, BlockTemplate, BlockView, Capacity, CellOutputWithOutPoint, CellTransaction, CellWithStatus, ChainInfo, DryRunResult, EpochNumber, EpochView, HeaderView, LiveCell, LockHashIndexState, Node, OutPoint, PeerState, Timestamp, Transaction, TransactionWithStatus, TxPoolInfo, Uint64, Version, @@ -346,6 +346,15 @@ impl RpcClient { .expect("rpc call calculate_dao_maximum_withdraw") .into() } + + pub fn get_cellbase_output_capacity_details(&self, hash: Byte32) -> BlockReward { + self.inner() + .lock() + .get_cellbase_output_capacity_details(hash.unpack()) + .call() + .expect("rpc call get_cellbase_output_capacity_details") + .expect("get_cellbase_output_capacity_details return none") + } } jsonrpc_client!(pub struct Inner { @@ -405,4 +414,5 @@ jsonrpc_client!(pub struct Inner { pub fn deindex_lock_hash(&mut self, lock_hash: H256) -> RpcRequest<()>; pub fn get_lock_hash_index_states(&mut self) -> RpcRequest>; pub fn calculate_dao_maximum_withdraw(&mut self, _out_point: OutPoint, _hash: H256) -> RpcRequest; + pub fn get_cellbase_output_capacity_details(&mut self, _hash: H256) -> RpcRequest>; }); diff --git a/test/src/specs/mining/fee.rs b/test/src/specs/mining/fee.rs index 8b13789179..baca852658 100644 --- a/test/src/specs/mining/fee.rs +++ b/test/src/specs/mining/fee.rs @@ -1 +1,355 @@ +use crate::utils::generate_utxo_set; +use crate::{Net, Node, Spec}; +use ckb_types::core::{BlockNumber, BlockView, Capacity, TransactionView}; +use ckb_types::packed::{Byte32, OutPoint}; +use ckb_types::prelude::Entity; +use ckb_types::prelude::*; +use std::collections::{HashMap, HashSet}; +pub struct FeeOfTransaction; + +impl Spec for FeeOfTransaction { + crate::name!("fee_of_transaction"); + + // Case: Only submit 1 transaction, and then wait for its proposed and committed + // + // 1. Submit transaction `tx` into transactions_pool after height `i` + // 2. Expect that the miner proposes `tx` within `block[i + 1]` + // 3. Expect that the miner commits `tx` within `block[i + 1 + PROPOSAL_WINDOW_CLOSEST]` + // 4. Expect that the miner receives the proposed reward of `tx` from + // `block[i + 1 + FINALIZATION_DELAY_LENGTH]` + // 5. Expect that the miner receives the committed reward of `tx` from + // `block[i + 1 + PROPOSAL_WINDOW_CLOSEST + FINALIZATION_DELAY_LENGTH]` + fn run(&self, net: Net) { + let node = &net.nodes[0]; + let closest = node.consensus().tx_proposal_window().closest(); + let finalization_delay_length = node.consensus().finalization_delay_length(); + + let txs = generate_utxo_set(node, 1).bang_random_fee(vec![node.always_success_cell_dep()]); + node.submit_transaction(&txs[0]); + + let number_to_submit = node.get_tip_block_number(); + let number_to_propose = number_to_submit + 1; + let number_to_commit = number_to_propose + closest; + node.generate_blocks(2 * finalization_delay_length as usize); + assert_block(node, number_to_propose, &txs, &[]); + assert_block(node, number_to_commit, &[], &txs); + assert_chain(node, &txs); + } +} + +pub struct FeeOfMaxBlockProposalsLimit; + +impl Spec for FeeOfMaxBlockProposalsLimit { + crate::name!("fee_of_max_block_proposals_limit"); + + // Case: Submit `MAX_BLOCK_PROPOSALS_LIMIT` transactions, and then wait for its proposed and committed + // + // 1. Submit `MAX_BLOCK_PROPOSALS_LIMIT` transactions into transactions_pool after height `i` + // 2. Expect that the miner receives the proposed reward of `tx` from + // `block[i + 1 + FINALIZATION_DELAY_LENGTH]` + fn run(&self, net: Net) { + let node = &net.nodes[0]; + let max_block_proposals_limit = node.consensus().max_block_proposals_limit(); + let finalization_delay_length = node.consensus().finalization_delay_length(); + let txs = generate_utxo_set(node, max_block_proposals_limit as usize) + .bang_random_fee(vec![node.always_success_cell_dep()]); + txs.iter().for_each(|tx| { + node.submit_transaction(tx); + }); + + let number_to_submit = node.get_tip_block_number(); + let number_to_propose = number_to_submit + 1; + node.generate_blocks(2 * finalization_delay_length as usize); + assert_block(node, number_to_propose, &txs, &[]); + assert_chain(node, &txs); + } +} + +pub struct FeeOfMultipleMaxBlockProposalsLimit; + +impl Spec for FeeOfMultipleMaxBlockProposalsLimit { + crate::name!("fee_of_multiple_max_block_proposals_limit"); + + // Case: Submit `3 * MAX_BLOCK_PROPOSALS_LIMIT` transactions, and then wait for its proposed and committed + // + // 1. Submit `3 * MAX_BLOCK_PROPOSALS_LIMIT` transactions into transactions_pool after height `i` + // 2. Expect that the miner propose those transactions in the next `3` blocks, every block + // contains `MAX_BLOCK_PROPOSALS_LIMIT` transactions + fn run(&self, net: Net) { + let node = &net.nodes[0]; + let max_block_proposals_limit = node.consensus().max_block_proposals_limit(); + let finalization_delay_length = node.consensus().finalization_delay_length(); + + let multiple = 3; + let txs = generate_utxo_set(node, (multiple * max_block_proposals_limit) as usize) + .bang_random_fee(vec![node.always_success_cell_dep()]); + txs.iter().for_each(|tx| { + node.submit_transaction(tx); + }); + + (0..multiple).for_each(|_| { + let block = node.new_block(None, None, None); + node.submit_block(&block.data()); + assert_eq!( + max_block_proposals_limit as usize, + block.union_proposal_ids_iter().count(), + ); + }); + + node.generate_blocks(2 * finalization_delay_length as usize); + assert_chain(node, &txs); + } +} + +pub struct ProposeButNotCommit; + +impl Spec for ProposeButNotCommit { + crate::name!("propose_but_not_commit"); + + crate::setup!(num_nodes: 2, connect_all: false); + + // Case: Propose a transaction but never commit it + fn run(&self, net: Net) { + let target_node = &net.nodes[0]; + let feed_node = &net.nodes[1]; + + // Construct a chain which proposed the target transaction in the tip block + let feed_blocks: Vec<_> = { + let txs = generate_utxo_set(feed_node, 1) + .bang_random_fee(vec![feed_node.always_success_cell_dep()]); + feed_node.submit_transaction(&txs[0]); + feed_node.generate_block(); + + (1..feed_node.get_tip_block_number()) + .map(|number| feed_node.get_block_by_number(number)) + .collect() + }; + + feed_blocks.iter().for_each(|block| { + target_node.submit_block(&block.data()); + }); + + let finalization_delay_length = feed_node.consensus().finalization_delay_length(); + target_node.generate_blocks(2 * finalization_delay_length as usize); + assert_block(target_node, target_node.get_tip_block_number(), &[], &[]); + assert_chain(target_node, &[]); + } +} + +pub struct ProposeDuplicated; + +impl Spec for ProposeDuplicated { + crate::name!("propose_duplicated"); + + // Case: Uncle contains a proposal, and the new block contains the same one. + fn run(&self, net: Net) { + let node = &net.nodes[0]; + let txs = generate_utxo_set(node, 1).bang_random_fee(vec![node.always_success_cell_dep()]); + let tx = &txs[0]; + + let uncle1 = { + let uncle = node + .new_block_builder(None, None, None) + .proposal(tx.proposal_short_id()) + .build() + .as_uncle(); + node.generate_block(); + uncle + }; + let uncle2 = { + let uncle = node + .new_block_builder(None, None, None) + .proposal(tx.proposal_short_id()) + .nonce(99999.pack()) + .build() + .as_uncle(); + node.generate_block(); + uncle + }; + + let block = node + .new_block_builder(None, None, None) + .uncle(uncle1) + .uncle(uncle2) + .build(); + node.submit_transaction(tx); + node.submit_block(&block.data()); + + let finalization_delay_length = node.consensus().finalization_delay_length(); + node.generate_blocks(2 * finalization_delay_length as usize); + assert_chain(node, &txs); + } +} + +// Check the given transactions were proposed and committed +fn assert_transactions_committed(node: &Node, transactions: &[TransactionView]) { + let tip_number = node.get_tip_block_number(); + let mut hashes: HashSet<_> = transactions.iter().map(|tx| tx.hash()).collect(); + (1..tip_number).for_each(|number| { + let block = node.get_block_by_number(number); + block.transactions().iter().skip(1).for_each(|tx| { + hashes.remove(&tx.hash()); + }); + }); + assert!(hashes.is_empty()); +} + +// Check the proposed-rewards and committed-rewards is correct +fn assert_chain_rewards(node: &Node) { + let mut fee_collector = FeeCollector::build(node); + let finalization_delay_length = node.consensus().finalization_delay_length(); + let tip_number = node.get_tip_block_number(); + assert!(tip_number > finalization_delay_length); + + for block_number in finalization_delay_length + 1..=tip_number { + let block_hash = node.rpc_client().get_block_hash(block_number).unwrap(); + let early_number = block_number - finalization_delay_length; + let early_block = node.get_block_by_number(early_number); + let proposed_fee: u64 = early_block + .union_proposal_ids_iter() + .map(|pid| { + let fee = fee_collector.remove(pid); + Capacity::shannons(fee) + .safe_mul_ratio(node.consensus().proposer_reward_ratio()) + .unwrap() + .as_u64() + }) + .sum(); + let committed_fee: u64 = early_block + .transactions() + .iter() + .skip(1) + .map(|tx| { + let fee = fee_collector.remove(tx.hash()); + fee - Capacity::shannons(fee) + .safe_mul_ratio(node.consensus().proposer_reward_ratio()) + .unwrap() + .as_u64() + }) + .sum(); + assert_proposed_reward(node, block_hash.clone(), proposed_fee); + assert_committed_reward(node, block_hash, committed_fee); + } +} + +fn assert_chain(node: &Node, transactions: &[TransactionView]) { + assert_transactions_committed(node, transactions); + assert_chain_rewards(node); +} + +fn assert_block( + node: &Node, + block_number: BlockNumber, + proposals: &[TransactionView], + committed: &[TransactionView], +) { + let block = node.get_block_by_number(block_number); + assert_proposals(&block, proposals); + assert_committed(&block, committed); +} + +fn assert_proposals(block: &BlockView, expected: &[TransactionView]) { + let mut actual_proposals: Vec<_> = block.union_proposal_ids_iter().collect(); + let mut expected_proposals: Vec<_> = expected.iter().map(|tx| tx.proposal_short_id()).collect(); + actual_proposals.sort_by(|a, b| a.as_bytes().cmp(&b.as_bytes())); + expected_proposals.sort_by(|a, b| a.as_bytes().cmp(&b.as_bytes())); + assert_eq!( + expected_proposals, + actual_proposals, + "assert_proposals failed at block[{}]", + block.number() + ); +} + +fn assert_committed(block: &BlockView, expected: &[TransactionView]) { + let actual_committed_hashes: Vec<_> = block + .transactions() + .iter() + .skip(1) + .map(|tx| tx.hash()) + .collect(); + let expected_committed_hashes: Vec<_> = expected.iter().map(|tx| tx.hash()).collect(); + assert_eq!( + &expected_committed_hashes, + &actual_committed_hashes, + "assert_committed failed at block[{}]", + block.number(), + ); +} + +fn assert_proposed_reward(node: &Node, block_hash: Byte32, expected: u64) { + let actual = node + .rpc_client() + .get_cellbase_output_capacity_details(block_hash.clone()) + .proposal_reward + .value(); + assert_eq!( + expected, + actual, + "assert_proposed_reward failed at block[{}]", + node.rpc_client() + .get_header(block_hash) + .unwrap() + .inner + .number + .value() + ); +} + +fn assert_committed_reward(node: &Node, block_hash: Byte32, expected: u64) { + let actual = node + .rpc_client() + .get_cellbase_output_capacity_details(block_hash.clone()) + .tx_fee + .value(); + assert_eq!( + expected, + actual, + "assert_committed_reward failed at block[{}]", + node.rpc_client() + .get_header(block_hash) + .unwrap() + .inner + .number + .value() + ); +} + +#[derive(Default)] +struct FeeCollector { + inner: HashMap, +} + +impl FeeCollector { + fn build(node: &Node) -> Self { + let mut this = Self::default(); + let mut cells = HashMap::new(); + for number in 0..node.get_tip_block_number() { + let block = node.get_block_by_number(number); + for (tx_index, tx) in block.transactions().iter().enumerate() { + for (index, output) in tx.outputs().into_iter().enumerate() { + let capacity: u64 = output.capacity().unpack(); + cells.insert(OutPoint::new(tx.hash(), index as u32), capacity); + } + + if tx_index == 0 { + continue; + } + let outputs_capacity = tx.outputs_capacity().unwrap().as_u64(); + let inputs_capacity: u64 = tx + .input_pts_iter() + .map(|previous_out_point| *cells.get(&previous_out_point).unwrap()) + .sum(); + let fee = inputs_capacity - outputs_capacity; + this.inner.insert(tx.hash().to_string(), fee); + this.inner.insert(tx.proposal_short_id().to_string(), fee); + } + } + this + } + + fn remove(&mut self, key: S) -> u64 { + self.inner.remove(&key.to_string()).unwrap_or(0u64) + } +} From 58abfacb60c7b7eb1390cecd297158a8449ddee2 Mon Sep 17 00:00:00 2001 From: keroro520 Date: Fri, 20 Sep 2019 10:56:54 +0800 Subject: [PATCH 6/7] test: Add comments for cases --- test/src/specs/mining/fee.rs | 51 ++++++++++++++---------------- test/src/specs/sync/chain_forks.rs | 32 +++++++++++++------ 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/test/src/specs/mining/fee.rs b/test/src/specs/mining/fee.rs index baca852658..42c540ebf4 100644 --- a/test/src/specs/mining/fee.rs +++ b/test/src/specs/mining/fee.rs @@ -1,6 +1,6 @@ use crate::utils::generate_utxo_set; use crate::{Net, Node, Spec}; -use ckb_types::core::{BlockNumber, BlockView, Capacity, TransactionView}; +use ckb_types::core::{BlockView, Capacity, TransactionView}; use ckb_types::packed::{Byte32, OutPoint}; use ckb_types::prelude::Entity; use ckb_types::prelude::*; @@ -32,9 +32,11 @@ impl Spec for FeeOfTransaction { let number_to_propose = number_to_submit + 1; let number_to_commit = number_to_propose + closest; node.generate_blocks(2 * finalization_delay_length as usize); - assert_block(node, number_to_propose, &txs, &[]); - assert_block(node, number_to_commit, &[], &txs); - assert_chain(node, &txs); + assert_proposals(&node.get_block_by_number(number_to_propose), &txs); + assert_committed(&node.get_block_by_number(number_to_commit), &txs); + + assert_transactions_committed(node, &txs); + assert_chain_rewards(node); } } @@ -61,8 +63,10 @@ impl Spec for FeeOfMaxBlockProposalsLimit { let number_to_submit = node.get_tip_block_number(); let number_to_propose = number_to_submit + 1; node.generate_blocks(2 * finalization_delay_length as usize); - assert_block(node, number_to_propose, &txs, &[]); - assert_chain(node, &txs); + assert_proposals(&node.get_block_by_number(number_to_propose), &txs); + + assert_transactions_committed(node, &txs); + assert_chain_rewards(node); } } @@ -98,7 +102,8 @@ impl Spec for FeeOfMultipleMaxBlockProposalsLimit { }); node.generate_blocks(2 * finalization_delay_length as usize); - assert_chain(node, &txs); + assert_transactions_committed(node, &txs); + assert_chain_rewards(node); } } @@ -114,7 +119,10 @@ impl Spec for ProposeButNotCommit { let target_node = &net.nodes[0]; let feed_node = &net.nodes[1]; - // Construct a chain which proposed the target transaction in the tip block + // We use `feed_node` to construct a chain proposed `txs` in the tip block. + // + // The returned `feed_blocks`, which represents the main fork of + // `feed_node`, only proposes `txs` in the last block and never commit let feed_blocks: Vec<_> = { let txs = generate_utxo_set(feed_node, 1) .bang_random_fee(vec![feed_node.always_success_cell_dep()]); @@ -126,14 +134,17 @@ impl Spec for ProposeButNotCommit { .collect() }; + // `target_node` propose `tx` feed_blocks.iter().for_each(|block| { target_node.submit_block(&block.data()); }); + // `target_node` keeps growing, but it will never commit `tx` since its transactions_pool + // have not `tx`. let finalization_delay_length = feed_node.consensus().finalization_delay_length(); target_node.generate_blocks(2 * finalization_delay_length as usize); - assert_block(target_node, target_node.get_tip_block_number(), &[], &[]); - assert_chain(target_node, &[]); + + assert_chain_rewards(target_node); } } @@ -178,7 +189,9 @@ impl Spec for ProposeDuplicated { let finalization_delay_length = node.consensus().finalization_delay_length(); node.generate_blocks(2 * finalization_delay_length as usize); - assert_chain(node, &txs); + + assert_transactions_committed(node, &txs); + assert_chain_rewards(node); } } @@ -233,22 +246,6 @@ fn assert_chain_rewards(node: &Node) { } } -fn assert_chain(node: &Node, transactions: &[TransactionView]) { - assert_transactions_committed(node, transactions); - assert_chain_rewards(node); -} - -fn assert_block( - node: &Node, - block_number: BlockNumber, - proposals: &[TransactionView], - committed: &[TransactionView], -) { - let block = node.get_block_by_number(block_number); - assert_proposals(&block, proposals); - assert_committed(&block, committed); -} - fn assert_proposals(block: &BlockView, expected: &[TransactionView]) { let mut actual_proposals: Vec<_> = block.union_proposal_ids_iter().collect(); let mut expected_proposals: Vec<_> = expected.iter().map(|tx| tx.proposal_short_id()).collect(); diff --git a/test/src/specs/sync/chain_forks.rs b/test/src/specs/sync/chain_forks.rs index 29457a7758..29a98e6395 100644 --- a/test/src/specs/sync/chain_forks.rs +++ b/test/src/specs/sync/chain_forks.rs @@ -629,33 +629,45 @@ impl Spec for ForkedTransaction { let fixed_point = node0.get_tip_block_number(); let tx = node1.new_transaction_spend_tip_cellbase(); - // node0 doesn't have `tx` => TxStatus: None - // node1 have `tx` on main-fork => TxStatus: Some(Committed) + // `node0` doesn't have `tx` => TxStatus: None { node0.generate_blocks(1 + 2 * finalization_delay_length as usize); - node1.submit_transaction(&tx); - node1.generate_blocks(2 * finalization_delay_length as usize); - let tx_status = node0.rpc_client().get_transaction(tx.hash()); assert!(tx_status.is_none(), "node0 maintains tx in unverified fork"); + } + + // `node1` have `tx` on main-fork => TxStatus: Some(Committed) + { + node1.submit_transaction(&tx); + node1.generate_blocks(2 * finalization_delay_length as usize); let tx_status = node1.rpc_client().get_transaction(tx.hash()).unwrap(); is_committed(&tx_status); } - // node0 have `tx` on unverified-fork => TxStatus: None - // node1 have `tx` on verified-fork => TxStatus: Some(Pending) + // `node0` have `tx` on unverified-fork only => TxStatus: None + // + // We submit the main-fork of `node1` to `node0`, that will be persisted as an + // unverified-fork inside `node0`. { (fixed_point..=node1.get_tip_block_number()).for_each(|number| { let block = node1.get_block_by_number(number); node0.submit_block(&block.data()); }); + let tx_status = node0.rpc_client().get_transaction(tx.hash()); + assert!(tx_status.is_none(), "node0 maintains tx in unverified fork"); + } + + // node1 have `tx` on verified-fork => TxStatus: Some(Pending) + // + // We submit the main-fork of `node0` to `node1`, that will trigger switching forks. Then + // the original main-fork of `node0` will become side verified-fork. And `tx` will be moved + // to gap-transactions-pool during switching forks + { (fixed_point..=node0.get_tip_block_number()).for_each(|number| { let block = node0.get_block_by_number(number); node1.submit_block(&block.data()); }); - let tx_status = node0.rpc_client().get_transaction(tx.hash()); - assert!(tx_status.is_none(), "node0 maintains tx in unverified fork"); let is_pending = |tx_status: &TransactionWithStatus| { let pending_status = TxStatus::pending(); tx_status.tx_status.status == pending_status.status @@ -663,7 +675,7 @@ impl Spec for ForkedTransaction { let tx_status = node1.rpc_client().get_transaction(tx.hash()).unwrap(); assert!( is_pending(&tx_status), - "node1 maintains tx in verified fork" + "node1 maintains tx in verified fork." ); } } From ac694e724780101e9c12291336db84190446ed8e Mon Sep 17 00:00:00 2001 From: keroro520 Date: Tue, 8 Oct 2019 22:08:51 +0800 Subject: [PATCH 7/7] chore(test): Adjust cellbase maturity format --- test/src/specs/mining/fee.rs | 10 +++++----- test/src/utils.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/src/specs/mining/fee.rs b/test/src/specs/mining/fee.rs index 42c540ebf4..408ddfedcd 100644 --- a/test/src/specs/mining/fee.rs +++ b/test/src/specs/mining/fee.rs @@ -20,7 +20,7 @@ impl Spec for FeeOfTransaction { // `block[i + 1 + FINALIZATION_DELAY_LENGTH]` // 5. Expect that the miner receives the committed reward of `tx` from // `block[i + 1 + PROPOSAL_WINDOW_CLOSEST + FINALIZATION_DELAY_LENGTH]` - fn run(&self, net: Net) { + fn run(&self, net: &mut Net) { let node = &net.nodes[0]; let closest = node.consensus().tx_proposal_window().closest(); let finalization_delay_length = node.consensus().finalization_delay_length(); @@ -50,7 +50,7 @@ impl Spec for FeeOfMaxBlockProposalsLimit { // 1. Submit `MAX_BLOCK_PROPOSALS_LIMIT` transactions into transactions_pool after height `i` // 2. Expect that the miner receives the proposed reward of `tx` from // `block[i + 1 + FINALIZATION_DELAY_LENGTH]` - fn run(&self, net: Net) { + fn run(&self, net: &mut Net) { let node = &net.nodes[0]; let max_block_proposals_limit = node.consensus().max_block_proposals_limit(); let finalization_delay_length = node.consensus().finalization_delay_length(); @@ -80,7 +80,7 @@ impl Spec for FeeOfMultipleMaxBlockProposalsLimit { // 1. Submit `3 * MAX_BLOCK_PROPOSALS_LIMIT` transactions into transactions_pool after height `i` // 2. Expect that the miner propose those transactions in the next `3` blocks, every block // contains `MAX_BLOCK_PROPOSALS_LIMIT` transactions - fn run(&self, net: Net) { + fn run(&self, net: &mut Net) { let node = &net.nodes[0]; let max_block_proposals_limit = node.consensus().max_block_proposals_limit(); let finalization_delay_length = node.consensus().finalization_delay_length(); @@ -115,7 +115,7 @@ impl Spec for ProposeButNotCommit { crate::setup!(num_nodes: 2, connect_all: false); // Case: Propose a transaction but never commit it - fn run(&self, net: Net) { + fn run(&self, net: &mut Net) { let target_node = &net.nodes[0]; let feed_node = &net.nodes[1]; @@ -154,7 +154,7 @@ impl Spec for ProposeDuplicated { crate::name!("propose_duplicated"); // Case: Uncle contains a proposal, and the new block contains the same one. - fn run(&self, net: Net) { + fn run(&self, net: &mut Net) { let node = &net.nodes[0]; let txs = generate_utxo_set(node, 1).bang_random_fee(vec![node.always_success_cell_dep()]); let tx = &txs[0]; diff --git a/test/src/utils.rs b/test/src/utils.rs index 54ee914a47..0f4a8014ee 100644 --- a/test/src/utils.rs +++ b/test/src/utils.rs @@ -210,16 +210,20 @@ pub fn temp_path() -> String { pub fn generate_utxo_set(node: &Node, n: usize) -> TXOSet { // Ensure all the cellbases will be used later are already mature. let cellbase_maturity = node.consensus().cellbase_maturity(); - node.generate_blocks(cellbase_maturity as usize); + node.generate_blocks(cellbase_maturity.index() as usize); // Explode these mature cellbases into multiple cells let mut n_outputs = 0; let mut txs = Vec::new(); while n > n_outputs { node.generate_block(); - let mature_number = node.get_tip_block_number() - cellbase_maturity; + let mature_number = node.get_tip_block_number() - cellbase_maturity.index(); let mature_block = node.get_block_by_number(mature_number); let mature_cellbase = mature_block.transaction(0).unwrap(); + if mature_cellbase.outputs().len() == 0 { + continue; + } + let mature_utxos: TXOSet = TXOSet::from(&mature_cellbase); let tx = mature_utxos.boom(vec![node.always_success_cell_dep()]); n_outputs += tx.outputs().len();