From f573bc5aea16ce636c4a49fa398f3a8c67c394cd Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 7 Jul 2025 19:43:30 +0300 Subject: [PATCH] Allow multiple FillOrder v1 txs to co-exist in the mempool --- Cargo.lock | 1 + chainstate/test-framework/src/helpers.rs | 280 +++++ chainstate/test-framework/src/lib.rs | 1 + .../src/tests/fungible_tokens_v1.rs | 7 +- .../test-suite/src/tests/helpers/mod.rs | 189 +--- .../test-suite/src/tests/input_commitments.rs | 9 +- .../test-suite/src/tests/orders_tests.rs | 7 +- mempool/Cargo.toml | 1 + mempool/src/pool/entry.rs | 83 +- mempool/src/pool/orphans/mod.rs | 11 +- mempool/src/pool/tests/mod.rs | 6 + mempool/src/pool/tests/orders_v1.rs | 986 ++++++++++++++++++ mempool/src/pool/tests/utils.rs | 1 - mempool/src/pool/tx_pool/reorg.rs | 5 +- mempool/src/pool/tx_pool/store/mod.rs | 1 + mempool/src/pool/tx_pool/tests/basic.rs | 5 - mempool/src/pool/tx_pool/tests/expiry.rs | 1 - mempool/src/pool/tx_pool/tests/utils.rs | 3 - mempool/src/pool/work_queue/test.rs | 2 - ...t_order_double_fill_with_same_dest_impl.py | 36 +- 20 files changed, 1365 insertions(+), 270 deletions(-) create mode 100644 chainstate/test-framework/src/helpers.rs create mode 100644 mempool/src/pool/tests/orders_v1.rs diff --git a/Cargo.lock b/Cargo.lock index b40cbab665..e7ed2f7f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4560,6 +4560,7 @@ dependencies = [ "chainstate-types", "common", "crypto", + "ctor", "hex", "jsonrpsee", "logging", diff --git a/chainstate/test-framework/src/helpers.rs b/chainstate/test-framework/src/helpers.rs new file mode 100644 index 0000000000..a7ccebdbab --- /dev/null +++ b/chainstate/test-framework/src/helpers.rs @@ -0,0 +1,280 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chainstate::BlockSource; +use chainstate_storage::{BlockchainStorageRead, Transactional}; +use common::{ + chain::{ + make_token_id, + output_value::OutputValue, + signature::inputsig::InputWitness, + tokens::{IsTokenFreezable, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply}, + AccountCommand, AccountNonce, AccountType, Block, Destination, GenBlock, OrderId, + OrdersVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{Amount, BlockHeight, Id, Idable}, +}; +use orders_accounting::OrdersAccountingDB; +use randomness::{CryptoRng, Rng, SliceRandom as _}; +use test_utils::random_ascii_alphanumeric_string; + +use crate::{get_output_value, TestFramework, TransactionBuilder}; + +// Note: this function will create 2 blocks +pub fn issue_and_mint_random_token_from_best_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + utxo_to_pay_fee: UtxoOutPoint, + amount_to_mint: Amount, + total_supply: TokenTotalSupply, + is_freezable: IsTokenFreezable, +) -> ( + TokenId, + /*tokens*/ UtxoOutPoint, + /*coins change*/ UtxoOutPoint, +) { + let best_block_id = tf.best_block_id(); + let issuance = { + let max_ticker_len = tf.chain_config().token_max_ticker_len(); + let max_dec_count = tf.chain_config().token_max_dec_count(); + let max_uri_len = tf.chain_config().token_max_uri_len(); + + let issuance = TokenIssuanceV1 { + token_ticker: random_ascii_alphanumeric_string(rng, 1..max_ticker_len) + .as_bytes() + .to_vec(), + number_of_decimals: rng.gen_range(1..max_dec_count), + metadata_uri: random_ascii_alphanumeric_string(rng, 1..max_uri_len).as_bytes().to_vec(), + total_supply, + is_freezable, + authority: Destination::AnyoneCanSpend, + }; + TokenIssuance::V1(issuance) + }; + + let (token_id, _, utxo_with_change) = + issue_token_from_block(rng, tf, best_block_id, utxo_to_pay_fee, issuance); + + let best_block_id = tf.best_block_id(); + let (_, mint_tx_id) = mint_tokens_in_block( + rng, + tf, + best_block_id, + utxo_with_change, + token_id, + amount_to_mint, + true, + ); + + ( + token_id, + UtxoOutPoint::new(mint_tx_id.into(), 0), + UtxoOutPoint::new(mint_tx_id.into(), 1), + ) +} + +pub fn issue_token_from_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + parent_block_id: Id, + utxo_to_pay_fee: UtxoOutPoint, + issuance: TokenIssuance, +) -> (TokenId, Id, UtxoOutPoint) { + let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); + + let fee_utxo_coins = + get_output_value(tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output()) + .unwrap() + .coin_amount() + .unwrap(); + + let tx = TransactionBuilder::new() + .add_input(utxo_to_pay_fee.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin((fee_utxo_coins - token_issuance_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::IssueFungibleToken(Box::new(issuance.clone()))) + .build(); + let parent_block_height = tf.gen_block_index(&parent_block_id).block_height(); + let token_id = make_token_id( + tf.chain_config(), + parent_block_height.next_height(), + tx.transaction().inputs(), + ) + .unwrap(); + let tx_id = tx.transaction().get_id(); + let block = tf + .make_block_builder() + .add_transaction(tx) + .with_parent(parent_block_id) + .build(rng); + let block_id = block.get_id(); + tf.process_block(block, BlockSource::Local).unwrap(); + + (token_id, block_id, UtxoOutPoint::new(tx_id.into(), 0)) +} + +pub fn mint_tokens_in_block( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + parent_block_id: Id, + utxo_to_pay_fee: UtxoOutPoint, + token_id: TokenId, + amount_to_mint: Amount, + produce_change: bool, +) -> (Id, Id) { + let token_supply_change_fee = + tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); + + let nonce = BlockchainStorageRead::get_account_nonce_count( + &tf.storage.transaction_ro().unwrap(), + AccountType::Token(token_id), + ) + .unwrap() + .map_or(AccountNonce::new(0), |n| n.increment().unwrap()); + + let tx_builder = TransactionBuilder::new() + .add_input( + TxInput::from_command(nonce, AccountCommand::MintTokens(token_id, amount_to_mint)), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_to_pay_fee.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, amount_to_mint), + Destination::AnyoneCanSpend, + )); + + let tx_builder = if produce_change { + let fee_utxo_coins = tf.coin_amount_from_utxo(&utxo_to_pay_fee); + + tx_builder.add_output(TxOutput::Transfer( + OutputValue::Coin((fee_utxo_coins - token_supply_change_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + } else { + tx_builder + }; + + let tx = tx_builder.build(); + let tx_id = tx.transaction().get_id(); + + let block = tf + .make_block_builder() + .add_transaction(tx) + .with_parent(parent_block_id) + .build(rng); + let block_id = block.get_id(); + tf.process_block(block, BlockSource::Local).unwrap(); + + (block_id, tx_id) +} + +/// Given the fill amount in the "ask" currency, return the filled amount in the "give" currency. +pub fn calculate_fill_order( + tf: &TestFramework, + order_id: &OrderId, + fill_amount_in_ask_currency: Amount, + orders_version: OrdersVersion, +) -> Amount { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order( + &orders_db, + *order_id, + fill_amount_in_ask_currency, + orders_version, + ) + .unwrap() +} + +/// Split an u128 value into the specified number of "randomish" parts (the min part size is half +/// the average part size). +pub fn split_u128(rng: &mut (impl Rng + CryptoRng), amount: u128, parts_count: usize) -> Vec { + assert!(parts_count > 0); + let mut result = Vec::with_capacity(parts_count); + let parts_count = parts_count as u128; + let min_part_amount = amount / parts_count / 2; + let mut remaining_amount_above_min = amount - min_part_amount * parts_count; + + for i in 0..parts_count { + let amount_part_above_min = if i == parts_count - 1 { + remaining_amount_above_min + } else { + rng.gen_range(0..remaining_amount_above_min / 2) + }; + + result.push(min_part_amount + amount_part_above_min); + remaining_amount_above_min -= amount_part_above_min; + } + + assert_eq!(result.iter().sum::(), amount); + + result.shuffle(rng); + result +} + +/// Start building a tx that will "split" the specified outpoint into the specified number of outpoints. +/// +/// The "fee" parameter only makes sense if the outpoint's currency is coins. +pub fn make_tx_builder_to_split_utxo( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + outpoint: UtxoOutPoint, + parts_count: usize, + fee: Amount, +) -> TransactionBuilder { + let utxo_output_value = get_output_value(tf.utxo(&outpoint).output()).unwrap(); + let utxo_amount = utxo_output_value.amount(); + + let output_amounts = split_u128(rng, (utxo_amount - fee).unwrap().into_atoms(), parts_count); + + let mut tx_builder = + TransactionBuilder::new().add_input(outpoint.into(), InputWitness::NoSignature(None)); + for output_amount in output_amounts { + tx_builder = tx_builder.add_output(TxOutput::Transfer( + output_value_with_amount(&utxo_output_value, Amount::from_atoms(output_amount)), + Destination::AnyoneCanSpend, + )) + } + + tx_builder +} + +pub fn split_utxo( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + outpoint: UtxoOutPoint, + parts_count: usize, +) -> Id { + let tx = make_tx_builder_to_split_utxo(rng, tf, outpoint, parts_count, Amount::ZERO).build(); + let tx_id = tx.transaction().get_id(); + + tf.make_block_builder().add_transaction(tx).build_and_process(rng).unwrap(); + tx_id +} + +pub fn output_value_with_amount(output_value: &OutputValue, new_amount: Amount) -> OutputValue { + match output_value { + OutputValue::Coin(_) => OutputValue::Coin(new_amount), + OutputValue::TokenV0(_) => { + panic!("Unexpected token v0"); + } + OutputValue::TokenV1(id, _) => OutputValue::TokenV1(*id, new_amount), + } +} diff --git a/chainstate/test-framework/src/lib.rs b/chainstate/test-framework/src/lib.rs index b0631153f0..bd27f7e45f 100644 --- a/chainstate/test-framework/src/lib.rs +++ b/chainstate/test-framework/src/lib.rs @@ -18,6 +18,7 @@ mod block_builder; mod framework; mod framework_builder; +pub mod helpers; mod key_manager; mod pos_block_builder; mod random_tx_maker; diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index 998a7a6e41..3768cef517 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -22,7 +22,10 @@ use chainstate::{ ConnectTransactionError, IOPolicyError, TokensError, }; use chainstate_storage::{BlockchainStorageRead, Transactional}; -use chainstate_test_framework::{TestFramework, TransactionBuilder}; +use chainstate_test_framework::{ + helpers::{issue_token_from_block, mint_tokens_in_block}, + TestFramework, TransactionBuilder, +}; use common::{ chain::{ make_token_id, @@ -57,8 +60,6 @@ use tx_verifier::{ CheckTransactionError, }; -use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; - fn make_issuance( rng: &mut impl Rng, supply: TokenTotalSupply, diff --git a/chainstate/test-suite/src/tests/helpers/mod.rs b/chainstate/test-suite/src/tests/helpers/mod.rs index bcddc80e4b..176214192e 100644 --- a/chainstate/test-suite/src/tests/helpers/mod.rs +++ b/chainstate/test-suite/src/tests/helpers/mod.rs @@ -13,26 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use chainstate::BlockSource; -use chainstate_storage::{BlockchainStorageRead, Transactional}; use chainstate_test_framework::{anyonecanspend_address, TestFramework, TransactionBuilder}; use common::{ chain::{ - block::timestamp::BlockTimestamp, - make_token_id, - output_value::OutputValue, - signature::inputsig::InputWitness, - timelock::OutputTimeLock, - tokens::{IsTokenFreezable, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply}, - AccountCommand, AccountNonce, AccountType, Block, Destination, GenBlock, OrderId, - OrdersVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + block::timestamp::BlockTimestamp, output_value::OutputValue, + signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination, Transaction, + TxInput, TxOutput, }, - primitives::{Amount, BlockDistance, BlockHeight, Id, Idable}, + primitives::{Amount, BlockDistance, Id, Idable}, }; use crypto::key::{KeyKind, PrivateKey}; -use orders_accounting::OrdersAccountingDB; use randomness::{CryptoRng, Rng}; -use test_utils::random_ascii_alphanumeric_string; pub mod block_creation_helpers; pub mod block_index_handle_impl; @@ -86,175 +77,3 @@ pub fn new_pub_key_destination(rng: &mut (impl Rng + CryptoRng)) -> Destination let (_, pub_key) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); Destination::PublicKey(pub_key) } - -pub fn issue_token_from_block( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - parent_block_id: Id, - utxo_to_pay_fee: UtxoOutPoint, - issuance: TokenIssuance, -) -> (TokenId, Id, UtxoOutPoint) { - let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); - - let fee_utxo_coins = chainstate_test_framework::get_output_value( - tf.chainstate.utxo(&utxo_to_pay_fee).unwrap().unwrap().output(), - ) - .unwrap() - .coin_amount() - .unwrap(); - - let tx = TransactionBuilder::new() - .add_input(utxo_to_pay_fee.into(), InputWitness::NoSignature(None)) - .add_output(TxOutput::Transfer( - OutputValue::Coin((fee_utxo_coins - token_issuance_fee).unwrap()), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::IssueFungibleToken(Box::new(issuance.clone()))) - .build(); - let parent_block_height = tf.gen_block_index(&parent_block_id).block_height(); - let token_id = make_token_id( - tf.chain_config(), - parent_block_height.next_height(), - tx.transaction().inputs(), - ) - .unwrap(); - let tx_id = tx.transaction().get_id(); - let block = tf - .make_block_builder() - .add_transaction(tx) - .with_parent(parent_block_id) - .build(rng); - let block_id = block.get_id(); - tf.process_block(block, BlockSource::Local).unwrap(); - - (token_id, block_id, UtxoOutPoint::new(tx_id.into(), 0)) -} - -pub fn mint_tokens_in_block( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - parent_block_id: Id, - utxo_to_pay_fee: UtxoOutPoint, - token_id: TokenId, - amount_to_mint: Amount, - produce_change: bool, -) -> (Id, Id) { - let token_supply_change_fee = - tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - - let nonce = BlockchainStorageRead::get_account_nonce_count( - &tf.storage.transaction_ro().unwrap(), - AccountType::Token(token_id), - ) - .unwrap() - .map_or(AccountNonce::new(0), |n| n.increment().unwrap()); - - let tx_builder = TransactionBuilder::new() - .add_input( - TxInput::from_command(nonce, AccountCommand::MintTokens(token_id, amount_to_mint)), - InputWitness::NoSignature(None), - ) - .add_input( - utxo_to_pay_fee.clone().into(), - InputWitness::NoSignature(None), - ) - .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, amount_to_mint), - Destination::AnyoneCanSpend, - )); - - let tx_builder = if produce_change { - let fee_utxo_coins = tf.coin_amount_from_utxo(&utxo_to_pay_fee); - - tx_builder.add_output(TxOutput::Transfer( - OutputValue::Coin((fee_utxo_coins - token_supply_change_fee).unwrap()), - Destination::AnyoneCanSpend, - )) - } else { - tx_builder - }; - - let tx = tx_builder.build(); - let tx_id = tx.transaction().get_id(); - - let block = tf - .make_block_builder() - .add_transaction(tx) - .with_parent(parent_block_id) - .build(rng); - let block_id = block.get_id(); - tf.process_block(block, BlockSource::Local).unwrap(); - - (block_id, tx_id) -} - -// Note: this function will create 2 blocks -pub fn issue_and_mint_random_token_from_best_block( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - utxo_to_pay_fee: UtxoOutPoint, - amount_to_mint: Amount, - total_supply: TokenTotalSupply, - is_freezable: IsTokenFreezable, -) -> ( - TokenId, - /*tokens*/ UtxoOutPoint, - /*coins change*/ UtxoOutPoint, -) { - let best_block_id = tf.best_block_id(); - let issuance = { - let max_ticker_len = tf.chain_config().token_max_ticker_len(); - let max_dec_count = tf.chain_config().token_max_dec_count(); - let max_uri_len = tf.chain_config().token_max_uri_len(); - - let issuance = TokenIssuanceV1 { - token_ticker: random_ascii_alphanumeric_string(rng, 1..max_ticker_len) - .as_bytes() - .to_vec(), - number_of_decimals: rng.gen_range(1..max_dec_count), - metadata_uri: random_ascii_alphanumeric_string(rng, 1..max_uri_len).as_bytes().to_vec(), - total_supply, - is_freezable, - authority: Destination::AnyoneCanSpend, - }; - TokenIssuance::V1(issuance) - }; - - let (token_id, _, utxo_with_change) = - issue_token_from_block(rng, tf, best_block_id, utxo_to_pay_fee, issuance); - - let best_block_id = tf.best_block_id(); - let (_, mint_tx_id) = mint_tokens_in_block( - rng, - tf, - best_block_id, - utxo_with_change, - token_id, - amount_to_mint, - true, - ); - - ( - token_id, - UtxoOutPoint::new(mint_tx_id.into(), 0), - UtxoOutPoint::new(mint_tx_id.into(), 1), - ) -} - -/// Given the fill amount in the "ask" currency, return the filled amount in the "give" currency. -pub fn calculate_fill_order( - tf: &TestFramework, - order_id: &OrderId, - fill_amount_in_ask_currency: Amount, - orders_version: OrdersVersion, -) -> Amount { - let db_tx = tf.storage.transaction_ro().unwrap(); - let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order( - &orders_db, - *order_id, - fill_amount_in_ask_currency, - orders_version, - ) - .unwrap() -} diff --git a/chainstate/test-suite/src/tests/input_commitments.rs b/chainstate/test-suite/src/tests/input_commitments.rs index 47b39f88b6..b55f323e8e 100644 --- a/chainstate/test-suite/src/tests/input_commitments.rs +++ b/chainstate/test-suite/src/tests/input_commitments.rs @@ -22,8 +22,9 @@ use chainstate::{ chainstate_interface::ChainstateInterface, BlockError, ChainstateError, ConnectTransactionError, }; use chainstate_test_framework::{ - create_chain_config_with_staking_pool, empty_witness, PoSBlockBuilder, TestFramework, - TransactionBuilder, + create_chain_config_with_staking_pool, empty_witness, + helpers::{calculate_fill_order, issue_and_mint_random_token_from_best_block}, + PoSBlockBuilder, TestFramework, TransactionBuilder, }; use common::{ chain::{ @@ -60,10 +61,6 @@ use tx_verifier::{ input_check::InputCheckErrorPayload, }; -use crate::tests::helpers::calculate_fill_order; - -use super::helpers::issue_and_mint_random_token_from_best_block; - #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index 29b7ccd180..f84e0d5973 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -21,7 +21,10 @@ use chainstate::{ BlockError, ChainstateError, CheckBlockError, CheckBlockTransactionsError, ConnectTransactionError, }; -use chainstate_test_framework::{output_value_amount, TestFramework, TransactionBuilder}; +use chainstate_test_framework::{ + helpers::{calculate_fill_order, issue_and_mint_random_token_from_best_block}, + output_value_amount, TestFramework, TransactionBuilder, +}; use common::{ address::pubkeyhash::PublicKeyHash, chain::{ @@ -52,8 +55,6 @@ use tx_verifier::{ CheckTransactionError, }; -use crate::tests::helpers::{calculate_fill_order, issue_and_mint_random_token_from_best_block}; - fn create_test_framework_with_orders( rng: &mut (impl Rng + CryptoRng), orders_version: OrdersVersion, diff --git a/mempool/Cargo.toml b/mempool/Cargo.toml index a58c90773c..714354ab61 100644 --- a/mempool/Cargo.toml +++ b/mempool/Cargo.toml @@ -48,5 +48,6 @@ chainstate-test-framework = { path = "../chainstate/test-framework" } crypto = { path = "../crypto" } test-utils = { path = "../test-utils" } +ctor.workspace = true mockall.workspace = true rstest.workspace = true diff --git a/mempool/src/pool/entry.rs b/mempool/src/pool/entry.rs index 1809785a57..9f4e6103bc 100644 --- a/mempool/src/pool/entry.rs +++ b/mempool/src/pool/entry.rs @@ -17,7 +17,7 @@ use std::num::NonZeroUsize; use common::{ chain::{ - AccountCommand, AccountNonce, AccountSpending, AccountType, OrderAccountCommand, + tokens::TokenId, AccountCommand, AccountNonce, AccountSpending, DelegationId, OrderId, SignedTransaction, Transaction, TxInput, UtxoOutPoint, }, primitives::{Id, Idable}, @@ -26,30 +26,38 @@ use common::{ use super::{Fee, Time, TxOptions, TxOrigin}; use crate::tx_origin::IsOrigin; -/// A dependency of a transaction on a previous account state. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxAccountDependency { - account: AccountType, - nonce: AccountNonce, -} - -impl TxAccountDependency { - pub fn new(account: AccountType, nonce: AccountNonce) -> Self { - TxAccountDependency { account, nonce } - } -} - /// A dependency of a transaction. May be another transaction or a previous account state. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum TxDependency { - DelegationAccount(TxAccountDependency), - TokenSupplyAccount(TxAccountDependency), - OrderAccount(TxAccountDependency), - // TODO: keep only V1 version after OrdersVersion::V1 is activated + DelegationAccount(DelegationId, AccountNonce), + TokenSupplyAccount(TokenId, AccountNonce), + // TODO: remove OrderV0Account after OrdersVersion::V1 is activated // https://github.com/mintlayer/mintlayer-core/issues/1901 - OrderV1Account(AccountType), + OrderV0Account(OrderId, AccountNonce), TxOutput(Id, u32), // TODO: Block reward? + + // Note that orders v1 are not needed here, because: + // 1) Since they don't use nonces, they don't create dependencies the way other account-based + // inputs do. + // 2) We could introduce a pseudo-dependency, e.g. in the form of an `enum { Fillable, Freezable, Concludable }` + // (we'd have to differentiate between dependencies that a tx requires vs those that it consumes, + // so e.g. a `FreezeOrder` input would require `Freezable` but consume both `Freezable` and `Fillable`). + // However, this doesn't seem to be useful because currently, with RBF disabled, `TxDependency` + // itself has limited use: + // a) It's used to check for conflicts (`check_mempool_policy` calls `conflicting_tx_ids` + // and returns `MempoolConflictError::Irreplacable` if any), but this check doesn't seem + // to be really needed, because a conflicting tx will always be rejected by the tx verifier + // anyway (also, since the tx verifier call happens first, it doesn't seem that this + // `Irreplacable` result is possible at all, unless it's a bug). + // Though technically, we could use the pseudo-dependency as an optimization, to avoid calling + // the tx verifier when we know it'll fail anyway. + // b) The orphan pool uses a TxDependency map to check whether tx's dependencies could have become + // satisfied. The pseudo-dependency won't be useful here at all. + // (Also note that even when RBF is finally implemented, RBFing an order-related tx will probably + // be based on re-using one of the UTXOs of the original tx, so tracking order inputs will probably + // not be needed anyway). + // TODO: return to this when enabling RBF. } impl TxDependency { @@ -62,33 +70,36 @@ impl TxDependency { fn from_account(account: &AccountSpending, nonce: AccountNonce) -> Self { match account { - AccountSpending::DelegationBalance(_, _) => { - Self::DelegationAccount(TxAccountDependency::new(account.into(), nonce)) + AccountSpending::DelegationBalance(delegation_id, _) => { + Self::DelegationAccount(*delegation_id, nonce) } } } fn from_account_cmd(cmd: &AccountCommand, nonce: AccountNonce) -> Self { match cmd { - AccountCommand::MintTokens(_, _) - | AccountCommand::UnmintTokens(_) - | AccountCommand::LockTokenSupply(_) - | AccountCommand::FreezeToken(_, _) - | AccountCommand::UnfreezeToken(_) - | AccountCommand::ChangeTokenMetadataUri(_, _) - | AccountCommand::ChangeTokenAuthority(_, _) => { - Self::TokenSupplyAccount(TxAccountDependency::new(cmd.into(), nonce)) + AccountCommand::MintTokens(token_id, _) + | AccountCommand::UnmintTokens(token_id) + | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::FreezeToken(token_id, _) + | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) + | AccountCommand::ChangeTokenAuthority(token_id, _) => { + Self::TokenSupplyAccount(*token_id, nonce) } - AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { - Self::OrderAccount(TxAccountDependency::new(cmd.into(), nonce)) + AccountCommand::ConcludeOrder(order_id) | AccountCommand::FillOrder(order_id, _, _) => { + Self::OrderV0Account(*order_id, nonce) } } } - fn from_order_account_cmd(cmd: &OrderAccountCommand) -> Self { - Self::OrderV1Account(cmd.clone().into()) - } fn from_input_requires(input: &TxInput) -> Option { + // TODO: the "nonce().decrement().map()" calls below don't seem to be correct, because + // returning None for account-based inputs with zero nonce means that such inputs will + // never be considered as conflicting. Perhaps we should store `Option` + // inside TxDependency's variants instead. + // (Note that this issue doesn't seem to have a noticeable impact at this moment, + // with disabled RBF). match input { TxInput::Utxo(utxo) => Self::from_utxo(utxo), TxInput::Account(acct) => { @@ -97,7 +108,7 @@ impl TxDependency { TxInput::AccountCommand(nonce, op) => { nonce.decrement().map(|nonce| Self::from_account_cmd(op, nonce)) } - TxInput::OrderAccountCommand(cmd) => Some(Self::from_order_account_cmd(cmd)), + TxInput::OrderAccountCommand(_) => None, } } @@ -106,7 +117,7 @@ impl TxDependency { TxInput::Utxo(_) => None, TxInput::Account(acct) => Some(Self::from_account(acct.account(), acct.nonce())), TxInput::AccountCommand(nonce, op) => Some(Self::from_account_cmd(op, *nonce)), - TxInput::OrderAccountCommand(cmd) => Some(Self::from_order_account_cmd(cmd)), + TxInput::OrderAccountCommand(_) => None, } } } diff --git a/mempool/src/pool/orphans/mod.rs b/mempool/src/pool/orphans/mod.rs index ad8a3498e8..c85c0f1b00 100644 --- a/mempool/src/pool/orphans/mod.rs +++ b/mempool/src/pool/orphans/mod.rs @@ -300,14 +300,17 @@ impl<'p> PoolEntry<'p> { /// Check no dependencies of given transaction are still in orphan pool so it can be considered /// as a candidate to move out. + /// + /// Note: this function is allowed to produce false positives - if true is returned but + /// the tx is still an orphan (e.g. due to account-based dependencies), the tx will be returned + /// to the orphan pool. pub fn is_ready(&self) -> bool { let entry = self.get(); !entry.requires().any(|dep| match dep { // Always consider account deps. TODO: can be optimized in the future - TxDependency::DelegationAccount(_) - | TxDependency::TokenSupplyAccount(_) - | TxDependency::OrderAccount(_) - | TxDependency::OrderV1Account(_) => false, + TxDependency::DelegationAccount(_, _) + | TxDependency::TokenSupplyAccount(_, _) + | TxDependency::OrderV0Account(_, _) => false, TxDependency::TxOutput(tx_id, _) => self.pool.maps.by_tx_id.contains_key(&tx_id), }) } diff --git a/mempool/src/pool/tests/mod.rs b/mempool/src/pool/tests/mod.rs index e901ac602c..ff322b8d0e 100644 --- a/mempool/src/pool/tests/mod.rs +++ b/mempool/src/pool/tests/mod.rs @@ -26,5 +26,11 @@ use common::{ }; mod basic; +mod orders_v1; mod orphans; mod utils; + +#[ctor::ctor] +fn init() { + logging::init_logging(); +} diff --git a/mempool/src/pool/tests/orders_v1.rs b/mempool/src/pool/tests/orders_v1.rs new file mode 100644 index 0000000000..52598dbd8e --- /dev/null +++ b/mempool/src/pool/tests/orders_v1.rs @@ -0,0 +1,986 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chainstate::{ + constraints_value_accumulator, tx_verifier::error::InputCheckErrorPayload, + ConnectTransactionError, +}; +use chainstate_test_framework::{ + helpers::{ + calculate_fill_order, issue_and_mint_random_token_from_best_block, + make_tx_builder_to_split_utxo, split_utxo, + }, + TestFrameworkBuilder, +}; +use common::chain::{ + make_order_id, + tokens::{IsTokenFreezable, TokenId, TokenTotalSupply}, + ChainstateUpgradeBuilder, OrderAccountCommand, OrderData, OrderId, OrdersVersion, UtxoOutPoint, +}; +use mintscript::translate::TranslationError; +use test_utils::{assert_matches, assert_matches_return_val}; + +use super::*; + +use crate::error::TxValidationError; + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn non_orphans(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_builder_with_orders_v1(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let tokens_src_id: OutPointSourceId = split_utxo(&mut rng, &mut tf, tokens_outpoint, 2).into(); + let coins_src_id: OutPointSourceId = split_utxo(&mut rng, &mut tf, coins_outpoint, 10).into(); + + let initial_ask_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let initial_give_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let order_id = { + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(initial_ask_amount), + OutputValue::TokenV1(token_id, initial_give_amount), + ); + + let tx = TransactionBuilder::new() + .add_input( + UtxoOutPoint::new(tokens_src_id.clone(), 0).into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::CreateOrder(Box::new(order_data))) + .build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + order_id + }; + let another_order_initial_ask_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let another_order_initial_give_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let another_order_id = { + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(another_order_initial_ask_amount), + OutputValue::TokenV1(token_id, another_order_initial_give_amount), + ); + + let tx = TransactionBuilder::new() + .add_input( + UtxoOutPoint::new(tokens_src_id.clone(), 1).into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::CreateOrder(Box::new(order_data))) + .build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + order_id + }; + + let fill_tx1 = make_fill_order_tx( + &mut tf, + order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 0), + None, + ); + let fill_tx2 = make_fill_order_tx( + &mut tf, + order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 1), + None, + ); + let fill_tx3 = make_fill_order_tx( + &mut tf, + order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 2), + None, + ); + let freeze_tx1 = make_freeze_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 3), + None, + ); + let freeze_tx2 = make_freeze_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 4), + None, + ); + let conclude_tx1 = make_conclude_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 5), + None, + ); + let conclude_tx2 = make_conclude_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 6), + None, + ); + let another_order_fill_tx = make_fill_order_tx( + &mut tf, + another_order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..another_order_initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 7), + None, + ); + let another_order_freeze_tx = make_freeze_order_tx( + &mut tf, + another_order_id, + UtxoOutPoint::new(coins_src_id.clone(), 8), + None, + ); + let another_order_conclude_tx = make_conclude_order_tx( + &mut tf, + another_order_id, + UtxoOutPoint::new(coins_src_id.clone(), 9), + None, + ); + + let chain_config = std::sync::Arc::clone(tf.chain_config()); + let mempool_config = create_mempool_config(); + let chainstate_handle = start_chainstate(tf.chainstate()); + let create_mempool = || { + Mempool::new( + Arc::clone(&chain_config), + mempool_config.clone(), + chainstate_handle.clone(), + Default::default(), + StoreMemoryUsageEstimator, + ) + }; + + { + let mut mempool = create_mempool(); + + // Can add another fill tx after a fill tx. + mempool.add_transaction_test(fill_tx1.clone()).unwrap().assert_in_mempool(); + mempool.add_transaction_test(fill_tx2.clone()).unwrap().assert_in_mempool(); + + // Can add a freeze after the fills. + mempool.add_transaction_test(freeze_tx1.clone()).unwrap().assert_in_mempool(); + + // Cannot add another freeze. + let err = mempool.add_transaction_test(freeze_tx2.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::AttemptedFreezeAlreadyFrozenOrder(_) + ) + )) + ); + + // Cannot add another fill after the freeze. + let err = mempool.add_transaction_test(fill_tx3.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::AttemptedFillFrozenOrder(_) + ), + _ + ) + )) + ); + + // Can add a conclude tx. + mempool.add_transaction_test(conclude_tx1.clone()).unwrap().assert_in_mempool(); + + // Cannot add another conclude tx. + let err = mempool.add_transaction_test(conclude_tx2.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Still cannot add another freeze. + let err = mempool.add_transaction_test(freeze_tx2.clone()).unwrap_err(); + let err_payload = assert_matches_return_val!( + &err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::InputCheck(err), + )), + err.error() + ); + assert_matches!( + err_payload, + InputCheckErrorPayload::Translation(TranslationError::OrderNotFound(_)) + ); + + // Still cannot add another fill. + let err = mempool.add_transaction_test(fill_tx3.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Can fill/freeze/conclude another order + mempool + .add_transaction_test(another_order_fill_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_freeze_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_conclude_tx.clone()) + .unwrap() + .assert_in_mempool(); + } + + // Same as above, but we start with a freeze + { + let mut mempool = create_mempool(); + + // Can add the freeze. + mempool.add_transaction_test(freeze_tx1.clone()).unwrap().assert_in_mempool(); + + // Cannot add another freeze. + let err = mempool.add_transaction_test(freeze_tx2.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::AttemptedFreezeAlreadyFrozenOrder(_) + ) + )) + ); + + // Cannot add a fill after the freeze. + let err = mempool.add_transaction_test(fill_tx1.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::AttemptedFillFrozenOrder(_) + ), + _ + ) + )) + ); + + // Can add a conclude tx. + mempool.add_transaction_test(conclude_tx1.clone()).unwrap().assert_in_mempool(); + + // Cannot add another conclude tx. + let err = mempool.add_transaction_test(conclude_tx2.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Still cannot add another freeze. + let err = mempool.add_transaction_test(freeze_tx2.clone()).unwrap_err(); + let err_payload = assert_matches_return_val!( + &err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::InputCheck(err), + )), + err.error() + ); + assert_matches!( + err_payload, + InputCheckErrorPayload::Translation(TranslationError::OrderNotFound(_)) + ); + + // Still cannot add another fill. + let err = mempool.add_transaction_test(fill_tx3.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Can fill/freeze/conclude another order + mempool + .add_transaction_test(another_order_fill_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_freeze_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_conclude_tx.clone()) + .unwrap() + .assert_in_mempool(); + } + + // Same as above, but we start with a conclude. + { + let mut mempool = create_mempool(); + + // Can add the conclude tx. + mempool.add_transaction_test(conclude_tx1.clone()).unwrap().assert_in_mempool(); + + // Cannot add another conclude tx. + let err = mempool.add_transaction_test(conclude_tx2.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Cannot add a fill after the conclude. + let err = mempool.add_transaction_test(fill_tx1.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + // Cannot add a freeze after the conclude. + let err = mempool.add_transaction_test(freeze_tx1.clone()).unwrap_err(); + let err_payload = assert_matches_return_val!( + &err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::InputCheck(err), + )), + err.error() + ); + assert_matches!( + err_payload, + InputCheckErrorPayload::Translation(TranslationError::OrderNotFound(_)) + ); + + // Can fill/freeze/conclude another order + mempool + .add_transaction_test(another_order_fill_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_freeze_tx.clone()) + .unwrap() + .assert_in_mempool(); + mempool + .add_transaction_test(another_order_conclude_tx.clone()) + .unwrap() + .assert_in_mempool(); + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orphans_with_missing_utxo(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_builder_with_orders_v1(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let coins_src_id: OutPointSourceId = split_utxo(&mut rng, &mut tf, coins_outpoint, 10).into(); + + let initial_ask_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let initial_give_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let order_id = { + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(initial_ask_amount), + OutputValue::TokenV1(token_id, initial_give_amount), + ); + + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order_data))) + .build(); + let order_id = make_order_id(tx.inputs()).unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + order_id + }; + + let missing_parent_tx = make_tx_builder_to_split_utxo( + &mut rng, + &mut tf, + UtxoOutPoint::new(coins_src_id.clone(), 0), + 10, + Amount::from_atoms(TEST_MIN_TX_RELAY_FEE_RATE.atoms_per_kb()), + ) + .build(); + let missing_parent_tx_src_id: OutPointSourceId = + missing_parent_tx.transaction().get_id().into(); + + let fill_tx1 = make_fill_order_tx( + &mut tf, + order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 1), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 0).into()), + ); + let fill_tx1_id = fill_tx1.transaction().get_id(); + let fill_tx2 = make_fill_order_tx( + &mut tf, + order_id, + token_id, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 2), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 1).into()), + ); + let fill_tx2_id = fill_tx2.transaction().get_id(); + let freeze_tx1 = make_freeze_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 3), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 2).into()), + ); + let freeze_tx1_id = freeze_tx1.transaction().get_id(); + let freeze_tx2 = make_freeze_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 4), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 3).into()), + ); + let freeze_tx2_id = freeze_tx2.transaction().get_id(); + let conclude_tx1 = make_conclude_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 5), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 4).into()), + ); + let conclude_tx1_id = conclude_tx1.transaction().get_id(); + let conclude_tx2 = make_conclude_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 6), + Some(UtxoOutPoint::new(missing_parent_tx_src_id.clone(), 5).into()), + ); + let conclude_tx2_id = conclude_tx2.transaction().get_id(); + + let unrelated_tx = TransactionBuilder::new() + .add_input( + UtxoOutPoint::new(coins_src_id.clone(), 7).into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Burn(OutputValue::Coin(Amount::from_atoms(1)))) + .build(); + + let chain_config = std::sync::Arc::clone(tf.chain_config()); + let mempool_config = create_mempool_config(); + let chainstate_handle = start_chainstate(tf.chainstate()); + let create_mempool = || { + Mempool::new( + Arc::clone(&chain_config), + mempool_config.clone(), + chainstate_handle.clone(), + Default::default(), + StoreMemoryUsageEstimator, + ) + }; + + // Note: + // 1) Below we can add, say, a freeze before a fill and they both end up in the orphan pool. + // This is because currently the orphan pool has no way of knowing whether 2 order txs can + // conflict with each other. + // 2) In the above-mentioned scenario, after the missing tx has been added to the mempool + // (so that the orphan txs are no longer orphans), the fill tx may or may not end up in the + // "normal" mempool, depending on the order in which the orphans will be handled. However, + // in both cases it will no longer be in the orphan pool. + + // Add 2 orphan fill txs. After the missing parent is also added, both fill txs should + // be in the mempool and none of them should be in the orphan pool. + { + let mut mempool = create_mempool(); + + mempool.add_transaction_test(fill_tx1.clone()).unwrap().assert_in_orphan_pool(); + mempool.add_transaction_test(fill_tx2.clone()).unwrap().assert_in_orphan_pool(); + + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + + // Add an unrelated tx; this shouldn't affect the orphans. + mempool.add_transaction_test(unrelated_tx.clone()).unwrap().assert_in_mempool(); + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + + // Now add the missing parent. + mempool + .add_transaction_test(missing_parent_tx.clone()) + .unwrap() + .assert_in_mempool(); + + assert!(mempool.contains_transaction(&fill_tx1_id)); + assert!(mempool.contains_transaction(&fill_tx2_id)); + assert!(!mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&fill_tx2_id)); + } + + // Add 2 orphan freeze and some fill txs. After the missing parent is also added, only + // one of the freeze txs should be in the mempool; none of the txs should be in the orphan pool. + { + let mut mempool = create_mempool(); + + mempool + .add_transaction_test(freeze_tx1.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool + .add_transaction_test(freeze_tx2.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool.add_transaction_test(fill_tx1.clone()).unwrap().assert_in_orphan_pool(); + mempool.add_transaction_test(fill_tx2.clone()).unwrap().assert_in_orphan_pool(); + + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(!mempool.contains_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_transaction(&freeze_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx2_id)); + + // Add an unrelated tx; this shouldn't affect the orphans. + mempool.add_transaction_test(unrelated_tx.clone()).unwrap().assert_in_mempool(); + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(!mempool.contains_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_transaction(&freeze_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx2_id)); + + // Now add the missing parent. + mempool + .add_transaction_test(missing_parent_tx.clone()) + .unwrap() + .assert_in_mempool(); + + // Only one of the freeze txs should be in the mempool. + let freeze1_in_mempool = mempool.contains_transaction(&freeze_tx1_id); + let freeze2_in_mempool = mempool.contains_transaction(&freeze_tx2_id); + assert_ne!(freeze1_in_mempool, freeze2_in_mempool); + + // None of the txs should be in the orphans pool. + assert!(!mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(!mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&freeze_tx2_id)); + } + + // Add 2 orphan conclude and some fill/freeze txs. After the missing parent is also added, only + // one of the conclude txs should be in the mempool; none of the txs should be in the orphan pool. + { + let mut mempool = create_mempool(); + + mempool + .add_transaction_test(conclude_tx1.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool + .add_transaction_test(conclude_tx2.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool + .add_transaction_test(freeze_tx1.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool + .add_transaction_test(freeze_tx2.clone()) + .unwrap() + .assert_in_orphan_pool(); + mempool.add_transaction_test(fill_tx1.clone()).unwrap().assert_in_orphan_pool(); + mempool.add_transaction_test(fill_tx2.clone()).unwrap().assert_in_orphan_pool(); + + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(!mempool.contains_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_transaction(&freeze_tx2_id)); + assert!(!mempool.contains_transaction(&conclude_tx1_id)); + assert!(!mempool.contains_transaction(&conclude_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx2_id)); + assert!(mempool.contains_orphan_transaction(&conclude_tx1_id)); + assert!(mempool.contains_orphan_transaction(&conclude_tx2_id)); + + // Add an unrelated tx; this shouldn't affect the orphans. + mempool.add_transaction_test(unrelated_tx.clone()).unwrap().assert_in_mempool(); + assert!(!mempool.contains_transaction(&fill_tx1_id)); + assert!(!mempool.contains_transaction(&fill_tx2_id)); + assert!(!mempool.contains_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_transaction(&freeze_tx2_id)); + assert!(!mempool.contains_transaction(&conclude_tx1_id)); + assert!(!mempool.contains_transaction(&conclude_tx2_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(mempool.contains_orphan_transaction(&freeze_tx2_id)); + assert!(mempool.contains_orphan_transaction(&conclude_tx1_id)); + assert!(mempool.contains_orphan_transaction(&conclude_tx2_id)); + + // Now add the missing parent. + mempool + .add_transaction_test(missing_parent_tx.clone()) + .unwrap() + .assert_in_mempool(); + + // Only one of the conclude txs should be in the mempool. + let conclude1_in_mempool = mempool.contains_transaction(&conclude_tx1_id); + let conclude2_in_mempool = mempool.contains_transaction(&conclude_tx2_id); + assert_ne!(conclude1_in_mempool, conclude2_in_mempool); + + // None of the txs should be in the orphans pool. + assert!(!mempool.contains_orphan_transaction(&fill_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&fill_tx2_id)); + assert!(!mempool.contains_orphan_transaction(&freeze_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&freeze_tx2_id)); + assert!(!mempool.contains_orphan_transaction(&conclude_tx1_id)); + assert!(!mempool.contains_orphan_transaction(&conclude_tx2_id)); + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orphans_with_missing_order(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_builder_with_orders_v1(&mut rng).build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let coins_src_id: OutPointSourceId = split_utxo(&mut rng, &mut tf, coins_outpoint, 10).into(); + + let initial_ask_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let initial_give_amount = Amount::from_atoms(rng.gen_range(1000..2000)); + let order_creation_tx = { + let fee_input = UtxoOutPoint::new(coins_src_id.clone(), 0); + let coins_amount = tf.coin_amount_from_utxo(&fee_input); + let fee = TEST_MIN_TX_RELAY_FEE_RATE.atoms_per_kb(); // the tx is expected to be less than 1 kb + let change = (coins_amount - Amount::from_atoms(fee)).unwrap(); + + TransactionBuilder::new() + .add_input(fee_input.into(), InputWitness::NoSignature(None)) + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(initial_ask_amount), + OutputValue::TokenV1(token_id, initial_give_amount), + )))) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change), + Destination::AnyoneCanSpend, + )) + .build() + }; + let order_id = make_order_id(order_creation_tx.inputs()).unwrap(); + + let fill_tx = make_fill_order_tx_from_initial_amounts( + &mut tf, + order_id, + token_id, + initial_ask_amount, + initial_give_amount, + Amount::from_atoms(rng.gen_range(10..initial_ask_amount.into_atoms() / 10)), + UtxoOutPoint::new(coins_src_id.clone(), 1), + None, + ); + let freeze_tx = make_freeze_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 2), + None, + ); + let conclude_tx = make_conclude_order_tx( + &mut tf, + order_id, + UtxoOutPoint::new(coins_src_id.clone(), 3), + None, + ); + + let chain_config = std::sync::Arc::clone(tf.chain_config()); + let mempool_config = create_mempool_config(); + let chainstate_handle = start_chainstate(tf.chainstate()); + let create_mempool = || { + Mempool::new( + Arc::clone(&chain_config), + mempool_config.clone(), + chainstate_handle.clone(), + Default::default(), + StoreMemoryUsageEstimator, + ) + }; + + // Note: at this moment missing order is considered a hard error, so the txs will just be rejected. + { + let mut mempool = create_mempool(); + + let err = mempool.add_transaction_test(fill_tx.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + + let err = mempool.add_transaction_test(freeze_tx.clone()).unwrap_err(); + let err_payload = assert_matches_return_val!( + &err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::InputCheck(err), + )), + err.error() + ); + assert_matches!( + err_payload, + InputCheckErrorPayload::Translation(TranslationError::OrderNotFound(_)) + ); + + let err = mempool.add_transaction_test(conclude_tx.clone()).unwrap_err(); + assert_matches!( + err, + Error::Validity(TxValidationError::TxValidation( + ConnectTransactionError::ConstrainedValueAccumulatorError( + constraints_value_accumulator::Error::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(_) + ), + _ + ) + )) + ); + } +} + +fn create_test_framework_builder_with_orders_v1( + rng: &mut (impl Rng + CryptoRng), +) -> TestFrameworkBuilder { + TestFramework::builder(rng).with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest().orders_version(OrdersVersion::V1).build(), + )]) + .unwrap(), + ) + .build(), + ) +} + +fn issue_and_mint_token_from_genesis( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, +) -> (TokenId, UtxoOutPoint, UtxoOutPoint) { + let genesis_block_id = tf.genesis().get_id(); + let utxo = UtxoOutPoint::new(genesis_block_id.into(), 0); + let to_mint = Amount::from_atoms(rng.gen_range(100..100_000_000)); + + issue_and_mint_random_token_from_best_block( + rng, + tf, + utxo, + to_mint, + TokenTotalSupply::Unlimited, + IsTokenFreezable::Yes, + ) +} + +fn make_fill_order_tx( + tf: &mut TestFramework, + order_id: OrderId, + token_id: TokenId, + fill_amount: Amount, + coins_outpoint: UtxoOutPoint, + additional_input: Option, +) -> SignedTransaction { + let filled_amount = calculate_fill_order(tf, &order_id, fill_amount, OrdersVersion::V1); + make_fill_order_tx_impl( + tf, + order_id, + token_id, + fill_amount, + filled_amount, + coins_outpoint, + additional_input, + ) +} + +#[allow(clippy::too_many_arguments)] +fn make_fill_order_tx_from_initial_amounts( + tf: &mut TestFramework, + order_id: OrderId, + token_id: TokenId, + initially_asked: Amount, + initially_given: Amount, + fill_amount: Amount, + coins_outpoint: UtxoOutPoint, + additional_input: Option, +) -> SignedTransaction { + let filled_amount = + orders_accounting::calculate_filled_amount(initially_asked, initially_given, fill_amount) + .unwrap(); + make_fill_order_tx_impl( + tf, + order_id, + token_id, + fill_amount, + filled_amount, + coins_outpoint, + additional_input, + ) +} + +fn make_fill_order_tx_impl( + tf: &mut TestFramework, + order_id: OrderId, + token_id: TokenId, + fill_amount: Amount, + filled_amount: Amount, + coins_outpoint: UtxoOutPoint, + additional_input: Option, +) -> SignedTransaction { + let fill_order_input = + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder(order_id, fill_amount)); + let coins_amount = tf.coin_amount_from_utxo(&coins_outpoint); + let fee = TEST_MIN_TX_RELAY_FEE_RATE.atoms_per_kb(); // the tx is expected to be less than 1 kb + let change = ((coins_amount - fill_amount).unwrap() - Amount::from_atoms(fee)).unwrap(); + + let mut tx_builder = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input(fill_order_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change), + Destination::AnyoneCanSpend, + )); + if let Some(additional_input) = additional_input { + tx_builder = tx_builder.add_input(additional_input, InputWitness::NoSignature(None)); + } + + tx_builder.build() +} + +fn make_freeze_order_tx( + tf: &mut TestFramework, + order_id: OrderId, + coins_outpoint: UtxoOutPoint, + additional_input: Option, +) -> SignedTransaction { + let freeze_order_input = + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)); + let coins_amount = tf.coin_amount_from_utxo(&coins_outpoint); + let fee = TEST_MIN_TX_RELAY_FEE_RATE.atoms_per_kb(); // the tx is expected to be less than 1 kb + let change = (coins_amount - Amount::from_atoms(fee)).unwrap(); + + let mut tx_builder = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input(freeze_order_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change), + Destination::AnyoneCanSpend, + )); + if let Some(additional_input) = additional_input { + tx_builder = tx_builder.add_input(additional_input, InputWitness::NoSignature(None)); + } + + tx_builder.build() +} + +fn make_conclude_order_tx( + tf: &mut TestFramework, + order_id: OrderId, + coins_outpoint: UtxoOutPoint, + additional_input: Option, +) -> SignedTransaction { + let conclude_order_input = + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)); + let coins_amount = tf.coin_amount_from_utxo(&coins_outpoint); + let fee = TEST_MIN_TX_RELAY_FEE_RATE.atoms_per_kb(); // the tx is expected to be less than 1 kb + let change = (coins_amount - Amount::from_atoms(fee)).unwrap(); + + let mut tx_builder = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input(conclude_order_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change), + Destination::AnyoneCanSpend, + )); + if let Some(additional_input) = additional_input { + tx_builder = tx_builder.add_input(additional_input, InputWitness::NoSignature(None)); + } + + tx_builder.build() +} diff --git a/mempool/src/pool/tests/utils.rs b/mempool/src/pool/tests/utils.rs index c637a9001d..5636650f71 100644 --- a/mempool/src/pool/tests/utils.rs +++ b/mempool/src/pool/tests/utils.rs @@ -28,7 +28,6 @@ use super::{Error, MemoryUsageEstimator, Mempool, TxEntry}; pub fn setup_with_chainstate( chainstate: Box, ) -> Mempool { - logging::init_logging(); let chain_config = std::sync::Arc::clone(chainstate.get_chain_config()); let mempool_config = create_mempool_config(); let chainstate_handle = start_chainstate(chainstate); diff --git a/mempool/src/pool/tx_pool/reorg.rs b/mempool/src/pool/tx_pool/reorg.rs index 366bab9415..c189811fa3 100644 --- a/mempool/src/pool/tx_pool/reorg.rs +++ b/mempool/src/pool/tx_pool/reorg.rs @@ -92,7 +92,7 @@ impl ReorgData { }) .collect(); - // The transactions are returned in the order of them being disconnected which is the + // The blocks are returned in the order of them being disconnected which is the // opposite of what we want for connecting, so we need to reverse the iterator here. self.disconnected .into_iter() @@ -170,6 +170,9 @@ pub fn handle_new_tip( return Ok(()); } + // TODO: also check whether any of the existing txs in the orphan pool are no longer orphans + // due to the newly mined txs. + match fetch_disconnected_txs(tx_pool, new_tip) { Ok(to_insert) => reorg_mempool_transactions(tx_pool, to_insert, finalizer), Err(err) => { diff --git a/mempool/src/pool/tx_pool/store/mod.rs b/mempool/src/pool/tx_pool/store/mod.rs index 2b9a6a3169..321f8aa16a 100644 --- a/mempool/src/pool/tx_pool/store/mod.rs +++ b/mempool/src/pool/tx_pool/store/mod.rs @@ -664,6 +664,7 @@ impl TxMempoolEntry { self.entry.creation_time() } + // Note: only the parents that are currently in the mempool are included here. pub fn parents(&self) -> impl Iterator> { self.parents.iter() } diff --git a/mempool/src/pool/tx_pool/tests/basic.rs b/mempool/src/pool/tx_pool/tests/basic.rs index b6e65a89a5..cc8c1955e2 100644 --- a/mempool/src/pool/tx_pool/tests/basic.rs +++ b/mempool/src/pool/tx_pool/tests/basic.rs @@ -17,7 +17,6 @@ use super::*; #[test] fn dummy_size() { - logging::init_logging(); log::debug!("1, 1: {}", estimate_tx_size(1, 1)); log::debug!("1, 2: {}", estimate_tx_size(1, 2)); log::debug!("1, 400: {}", estimate_tx_size(1, 400)); @@ -590,7 +589,6 @@ async fn not_too_many_conflicts(#[case] seed: Seed) -> anyhow::Result<()> { #[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn rolling_fee(#[case] seed: Seed) -> anyhow::Result<()> { - logging::init_logging(); let mock_time = Arc::new(SeqCstAtomicU64::new(0)); let mock_clock = mocked_time_getter_seconds(Arc::clone(&mock_time)); let mut mock_usage = MockMemoryUsageEstimator::new(); @@ -1221,8 +1219,6 @@ fn check_txs_sorted_by_descendant_sore(tx_pool: &TxPool) { #[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mempool_full_mock(#[case] seed: Seed) -> anyhow::Result<()> { - logging::init_logging(); - let mut rng = make_seedable_rng(seed); let tf = TestFramework::builder(&mut rng).build(); let genesis = tf.genesis(); @@ -1271,7 +1267,6 @@ async fn mempool_full_mock(#[case] seed: Seed) -> anyhow::Result<()> { #[case::fail(Seed(1))] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mempool_full_real(#[case] seed: Seed) { - logging::init_logging(); let mut rng = make_seedable_rng(seed); let num_txs = rng.gen_range(5..20); diff --git a/mempool/src/pool/tx_pool/tests/expiry.rs b/mempool/src/pool/tx_pool/tests/expiry.rs index 83662fdea7..788347c6ca 100644 --- a/mempool/src/pool/tx_pool/tests/expiry.rs +++ b/mempool/src/pool/tx_pool/tests/expiry.rs @@ -25,7 +25,6 @@ use ::utils::atomics::SeqCstAtomicU64; async fn descendant_of_expired_entry(#[case] seed: Seed) -> anyhow::Result<()> { let mock_time = Arc::new(SeqCstAtomicU64::new(0)); let mock_clock = mocked_time_getter_seconds(Arc::clone(&mock_time)); - logging::init_logging(); let mut rng = make_seedable_rng(seed); let tf = TestFramework::builder(&mut rng).build(); diff --git a/mempool/src/pool/tx_pool/tests/utils.rs b/mempool/src/pool/tx_pool/tests/utils.rs index e55bff0c9e..12188b4379 100644 --- a/mempool/src/pool/tx_pool/tests/utils.rs +++ b/mempool/src/pool/tx_pool/tests/utils.rs @@ -339,7 +339,6 @@ pub fn make_test_block( } pub fn setup() -> TxPool { - logging::init_logging(); let chain_config = Arc::new(common::chain::config::create_unit_test_config()); let chainstate_interface = start_chainstate_with_config(Arc::clone(&chain_config)); TxPool::new( @@ -352,7 +351,6 @@ pub fn setup() -> TxPool { } pub fn setup_with_min_tx_relay_fee_rate(fee_rate: FeeRate) -> TxPool { - logging::init_logging(); let chain_config = Arc::new(common::chain::config::create_unit_test_config()); let mempool_config = MempoolConfig { min_tx_relay_fee_rate: fee_rate.into(), @@ -370,7 +368,6 @@ pub fn setup_with_min_tx_relay_fee_rate(fee_rate: FeeRate) -> TxPool, ) -> TxPool { - logging::init_logging(); let chain_config = Arc::clone(chainstate.get_chain_config()); let chainstate_handle = start_chainstate(chainstate); TxPool::new( diff --git a/mempool/src/pool/work_queue/test.rs b/mempool/src/pool/work_queue/test.rs index b0d7a3a635..3c28a51e1f 100644 --- a/mempool/src/pool/work_queue/test.rs +++ b/mempool/src/pool/work_queue/test.rs @@ -118,7 +118,6 @@ impl PeerIdSupply { #[trace] #[case(Seed::from_entropy())] fn simulation(#[case] seed: Seed) { - logging::init_logging(); let mut rng = make_seedable_rng(seed); let mut peer_supply = PeerIdSupply::new(); @@ -198,7 +197,6 @@ fn scheduling_fairness_full_queues(#[case] seed: Seed) { // Minimum number of work items in each peer's queue at the start const MIN_WORK: usize = 100; - logging::init_logging(); let mut rng = make_seedable_rng(seed); let num_peers: usize = rng.gen_range(2..=8); let peer1 = PeerId::from_u64(1); diff --git a/test/functional/wallet_order_double_fill_with_same_dest_impl.py b/test/functional/wallet_order_double_fill_with_same_dest_impl.py index 4e1e738b9c..75052c68c6 100644 --- a/test/functional/wallet_order_double_fill_with_same_dest_impl.py +++ b/test/functional/wallet_order_double_fill_with_same_dest_impl.py @@ -173,45 +173,41 @@ async def async_test(self): assert_not_in("Tokens", balance) fill_dest_address = await wallet.new_address() + # This will buy one token. + fill_amount = 2 - # Buy 1 token - result = await wallet.fill_order(order_id, 2, fill_dest_address) + # Perform the fill. + result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) fill_tx1_id = result['result']['tx_id'] assert fill_tx1_id is not None if self.use_orders_v1: - # Immediately buy 1 more token using the same destination address. Since the wallet also uses - # the passed destination as the destination in the FillOrder input, mempool will think that the - # second transaction conflicts with the first one. - result = await wallet.fill_order(order_id, 2, fill_dest_address) - assert_in("Mempool error: Transaction conflicts with another, irreplaceable transaction", result['error']['message']) + # Perform another fill for the same amount. + result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) + fill_tx2_id = result['result']['tx_id'] + assert fill_tx2_id is not None - # We are able to successfully generate a block. + # We're able to successfully mine a block, which will contain both transactions. self.generate_block() assert_in("Success", await wallet.sync()) - - # Try creating the transaction again, in a new block. Now it should succeed. - result = await wallet.fill_order(order_id, 2, fill_dest_address) - fill_tx2_id = result['result']['tx_id'] - assert fill_tx2_id is not None else: - # In orders v0 the destination shouldn't be a problem due to nonces. - # However, at this moment the wallet gets the nonce from the chainstate only, - # so creating another "fill" tx when the previos one hasn't been mined yet + # Perform another fill for the same amount. + # But note that orders v0 use nonces and at this moment the wallet gets the current nonce from + # the chainstate only, so creating another "fill" tx when the previos one hasn't been mined yet # will use the same nonce. - result = await wallet.fill_order(order_id, 2, fill_dest_address) + result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) assert_in("Mempool error: Nonce is not incremental", result['error']['message']) self.generate_block() assert_in("Success", await wallet.sync()) # After the first tx has been mined, a new one will be created with the correct nonce. - result = await wallet.fill_order(order_id, 2, fill_dest_address) + result = await wallet.fill_order(order_id, fill_amount, fill_dest_address) fill_tx2_id = result['result']['tx_id'] assert fill_tx2_id is not None - self.generate_block() - assert_in("Success", await wallet.sync()) + self.generate_block() + assert_in("Success", await wallet.sync()) balance = await wallet.get_balance() assert_in(f"Coins amount: 146.99", balance)