mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
Merge b0222bbb49 into merged_master (Bitcoin PR bitcoin/bitcoin#30239)
This commit is contained in:
commit
744c517dac
22 changed files with 1236 additions and 10 deletions
12
doc/release-notes-30239.md
Normal file
12
doc/release-notes-30239.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
P2P and network changes
|
||||
-----------------------
|
||||
|
||||
Ephemeral dust is a new concept that allows a single
|
||||
dust output in a transaction, provided the transaction
|
||||
is zero fee. In order to spend any unconfirmed outputs
|
||||
from this transaction, the spender must also spend
|
||||
this dust in addition to any other outputs.
|
||||
|
||||
In other words, this type of transaction
|
||||
should be created in a transaction package where
|
||||
the dust is both created and spent simultaneously.
|
||||
|
|
@ -304,6 +304,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
|
|||
node/warnings.cpp
|
||||
noui.cpp
|
||||
pegins.cpp
|
||||
policy/ephemeral_policy.cpp
|
||||
policy/fees.cpp
|
||||
policy/fees_args.cpp
|
||||
policy/packages.cpp
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ add_executable(bench_bitcoin
|
|||
load_external.cpp
|
||||
lockedpool.cpp
|
||||
logging.cpp
|
||||
mempool_ephemeral_spends.cpp
|
||||
mempool_eviction.cpp
|
||||
mempool_stress.cpp
|
||||
merkle_root.cpp
|
||||
|
|
|
|||
84
src/bench/mempool_ephemeral_spends.cpp
Normal file
84
src/bench/mempool_ephemeral_spends.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2011-2022 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <bench/bench.h>
|
||||
#include <consensus/amount.h>
|
||||
#include <kernel/cs_main.h>
|
||||
#include <policy/ephemeral_policy.h>
|
||||
#include <policy/policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <script/script.h>
|
||||
#include <sync.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <txmempool.h>
|
||||
#include <util/check.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
|
||||
static void AddTx(const CTransactionRef& tx, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs)
|
||||
{
|
||||
int64_t nTime{0};
|
||||
unsigned int nHeight{1};
|
||||
uint64_t sequence{0};
|
||||
bool spendsCoinbase{false};
|
||||
unsigned int sigOpCost{4};
|
||||
uint64_t fee{0};
|
||||
LockPoints lp;
|
||||
std::set<std::pair<uint256, COutPoint>> setPeginsSpent;
|
||||
pool.addUnchecked(CTxMemPoolEntry(
|
||||
tx, fee, nTime, nHeight, sequence,
|
||||
spendsCoinbase, sigOpCost, lp, setPeginsSpent));
|
||||
}
|
||||
|
||||
static void MempoolCheckEphemeralSpends(benchmark::Bench& bench)
|
||||
{
|
||||
const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
|
||||
|
||||
int number_outputs{1000};
|
||||
if (bench.complexityN() > 1) {
|
||||
number_outputs = static_cast<int>(bench.complexityN());
|
||||
}
|
||||
|
||||
// Tx with many outputs
|
||||
CMutableTransaction tx1 = CMutableTransaction();
|
||||
tx1.vin.resize(1);
|
||||
tx1.vout.resize(number_outputs);
|
||||
for (size_t i = 0; i < tx1.vout.size(); i++) {
|
||||
tx1.vout[i].scriptPubKey = CScript();
|
||||
// Each output progressively larger
|
||||
tx1.vout[i].nValue = i * CENT;
|
||||
}
|
||||
|
||||
const auto& parent_txid = tx1.GetHash();
|
||||
|
||||
// Spends all outputs of tx1, other details don't matter
|
||||
CMutableTransaction tx2 = CMutableTransaction();
|
||||
tx2.vin.resize(tx1.vout.size());
|
||||
for (size_t i = 0; i < tx2.vin.size(); i++) {
|
||||
tx2.vin[0].prevout.hash = parent_txid;
|
||||
tx2.vin[0].prevout.n = i;
|
||||
}
|
||||
tx2.vout.resize(1);
|
||||
|
||||
CTxMemPool& pool = *Assert(testing_setup->m_node.mempool);
|
||||
LOCK2(cs_main, pool.cs);
|
||||
// Create transaction references outside the "hot loop"
|
||||
const CTransactionRef tx1_r{MakeTransactionRef(tx1)};
|
||||
const CTransactionRef tx2_r{MakeTransactionRef(tx2)};
|
||||
|
||||
AddTx(tx1_r, pool);
|
||||
|
||||
uint32_t iteration{0};
|
||||
|
||||
bench.run([&]() NO_THREAD_SAFETY_ANALYSIS {
|
||||
|
||||
CheckEphemeralSpends({tx2_r}, /*dust_relay_rate=*/CFeeRate(iteration * COIN / 10), pool);
|
||||
iteration++;
|
||||
});
|
||||
}
|
||||
|
||||
BENCHMARK(MempoolCheckEphemeralSpends, benchmark::PriorityLevel::HIGH);
|
||||
|
|
@ -45,6 +45,7 @@ add_library(bitcoinkernel
|
|||
../node/chainstate.cpp
|
||||
../node/utxo_snapshot.cpp
|
||||
../pegins.cpp
|
||||
../policy/ephemeral_policy.cpp
|
||||
../policy/feerate.cpp
|
||||
../policy/packages.cpp
|
||||
../policy/policy.cpp
|
||||
|
|
|
|||
81
src/policy/ephemeral_policy.cpp
Normal file
81
src/policy/ephemeral_policy.cpp
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2024-present The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <policy/ephemeral_policy.h>
|
||||
#include <policy/policy.h>
|
||||
|
||||
bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate)
|
||||
{
|
||||
return std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& output) {
|
||||
return output.nAsset.IsExplicit() && output.nAsset.GetAsset() == policyAsset && IsDust(output, dust_relay_rate);
|
||||
});
|
||||
}
|
||||
|
||||
bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state)
|
||||
{
|
||||
// We never want to give incentives to mine this transaction alone
|
||||
if ((base_fee != 0 || mod_fee != 0) && HasDust(tx, dust_relay_rate)) {
|
||||
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust", "tx with dust output must be 0-fee");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool)
|
||||
{
|
||||
if (!Assume(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;}))) {
|
||||
// Bail out of spend checks if caller gave us an invalid package
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::map<Txid, CTransactionRef> map_txid_ref;
|
||||
for (const auto& tx : package) {
|
||||
map_txid_ref[tx->GetHash()] = tx;
|
||||
}
|
||||
|
||||
for (const auto& tx : package) {
|
||||
Txid txid = tx->GetHash();
|
||||
std::unordered_set<Txid, SaltedTxidHasher> processed_parent_set;
|
||||
std::unordered_set<COutPoint, SaltedOutpointHasher> unspent_parent_dust;
|
||||
|
||||
for (const auto& tx_input : tx->vin) {
|
||||
const Txid& parent_txid{tx_input.prevout.hash};
|
||||
// Skip parents we've already checked dust for
|
||||
if (processed_parent_set.contains(parent_txid)) continue;
|
||||
|
||||
// We look for an in-package or in-mempool dependency
|
||||
CTransactionRef parent_ref = nullptr;
|
||||
if (auto it = map_txid_ref.find(parent_txid); it != map_txid_ref.end()) {
|
||||
parent_ref = it->second;
|
||||
} else {
|
||||
parent_ref = tx_pool.get(parent_txid);
|
||||
}
|
||||
|
||||
// Check for dust on parents
|
||||
if (parent_ref) {
|
||||
for (uint32_t out_index = 0; out_index < parent_ref->vout.size(); out_index++) {
|
||||
const auto& tx_output = parent_ref->vout[out_index];
|
||||
// ELEMENTS: only treat policy asset outputs as ephemeral dust
|
||||
if (tx_output.nAsset.IsExplicit() && tx_output.nAsset.GetAsset() == policyAsset && IsDust(tx_output, dust_relay_rate)) {
|
||||
unspent_parent_dust.insert(COutPoint(parent_txid, out_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processed_parent_set.insert(parent_txid);
|
||||
}
|
||||
|
||||
// Now that we have gathered parents' dust, make sure it's spent
|
||||
// by the child
|
||||
for (const auto& tx_input : tx->vin) {
|
||||
unspent_parent_dust.erase(tx_input.prevout);
|
||||
}
|
||||
|
||||
if (!unspent_parent_dust.empty()) {
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
55
src/policy/ephemeral_policy.h
Normal file
55
src/policy/ephemeral_policy.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2024-present The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_POLICY_EPHEMERAL_POLICY_H
|
||||
#define BITCOIN_POLICY_EPHEMERAL_POLICY_H
|
||||
|
||||
#include <policy/packages.h>
|
||||
#include <policy/policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <txmempool.h>
|
||||
|
||||
/** These utility functions ensure that ephemeral dust is safely
|
||||
* created and spent without unduly risking them entering the utxo
|
||||
* set.
|
||||
|
||||
* This is ensured by requiring:
|
||||
* - CheckValidEphemeralTx checks are respected
|
||||
* - The parent has no child (and 0-fee as implied above to disincentivize mining)
|
||||
* - OR the parent transaction has exactly one child, and the dust is spent by that child
|
||||
*
|
||||
* Imagine three transactions:
|
||||
* TxA, 0-fee with two outputs, one non-dust, one dust
|
||||
* TxB, spends TxA's non-dust
|
||||
* TxC, spends TxA's dust
|
||||
*
|
||||
* All the dust is spent if TxA+TxB+TxC is accepted, but the mining template may just pick
|
||||
* up TxA+TxB rather than the three "legal configurations:
|
||||
* 1) None
|
||||
* 2) TxA+TxB+TxC
|
||||
* 3) TxA+TxC
|
||||
* By requiring the child transaction to sweep any dust from the parent txn, we ensure that
|
||||
* there is a single child only, and this child, or the child's descendants,
|
||||
* are the only way to bring fees.
|
||||
*/
|
||||
|
||||
/** Returns true if transaction contains dust */
|
||||
bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate);
|
||||
|
||||
/* All the following checks are only called if standardness rules are being applied. */
|
||||
|
||||
/** Must be called for each transaction once transaction fees are known.
|
||||
* Does context-less checks about a single transaction.
|
||||
* Returns false if the fee is non-zero and dust exists, populating state. True otherwise.
|
||||
*/
|
||||
bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state);
|
||||
|
||||
/** Must be called for each transaction(package) if any dust is in the package.
|
||||
* Checks that each transaction's parents have their dust spent by the child,
|
||||
* where parents are either in the mempool or in the package itself.
|
||||
* The function returns std::nullopt if all dust is properly spent, or the txid of the violating child spend.
|
||||
*/
|
||||
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool);
|
||||
|
||||
#endif // BITCOIN_POLICY_EPHEMERAL_POLICY_H
|
||||
|
|
@ -147,6 +147,7 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
|
|||
|
||||
const CChainParams& params = Params();
|
||||
unsigned int nDataOut = 0;
|
||||
unsigned int num_dust_outputs{0};
|
||||
TxoutType whichType;
|
||||
for (const CTxOut& txout : tx.vout) {
|
||||
if (!::IsStandard(txout.scriptPubKey, max_datacarrier_bytes, whichType)) {
|
||||
|
|
@ -160,11 +161,16 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
|
|||
reason = "bare-multisig";
|
||||
return false;
|
||||
} else if ((txout.nAsset.IsExplicit() && txout.nAsset.GetAsset() == policyAsset) && IsDust(txout, dust_relay_fee)) {
|
||||
reason = "dust";
|
||||
return false;
|
||||
num_dust_outputs++;
|
||||
}
|
||||
}
|
||||
|
||||
// Only MAX_DUST_OUTPUTS_PER_TX dust is permitted(on otherwise valid ephemeral dust)
|
||||
if (num_dust_outputs > MAX_DUST_OUTPUTS_PER_TX) {
|
||||
reason = "dust";
|
||||
return false;
|
||||
}
|
||||
|
||||
// only one OP_RETURN txout is permitted
|
||||
if (!params.GetMultiDataPermitted() && nDataOut > 1) {
|
||||
reason = "multi-op-return";
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ static const unsigned int MAX_OP_RETURN_RELAY = 83;
|
|||
*/
|
||||
static constexpr unsigned int EXTRA_DESCENDANT_TX_SIZE_LIMIT{10000};
|
||||
|
||||
/**
|
||||
* Maximum number of ephemeral dust outputs allowed.
|
||||
*/
|
||||
static constexpr unsigned int MAX_DUST_OUTPUTS_PER_TX{1};
|
||||
|
||||
/**
|
||||
* Mandatory script verification flags that all new transactions must comply with for
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
#include <node/miner.h>
|
||||
#include <policy/policy.h>
|
||||
#include <node/warnings.h>
|
||||
#include <policy/ephemeral_policy.h>
|
||||
#include <pow.h>
|
||||
#include <random.h>
|
||||
#include <rpc/blockchain.h>
|
||||
|
|
@ -515,7 +516,15 @@ static RPCHelpMan prioritisetransaction()
|
|||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is no longer supported, dummy argument to prioritisetransaction must be 0.");
|
||||
}
|
||||
|
||||
EnsureAnyMemPool(request.context).PrioritiseTransaction(hash, nAmount);
|
||||
CTxMemPool& mempool = EnsureAnyMemPool(request.context);
|
||||
|
||||
// Non-0 fee dust transactions are not allowed for entry, and modification not allowed afterwards
|
||||
const auto& tx = mempool.get(hash);
|
||||
if (tx && HasDust(tx, mempool.m_opts.dust_relay_feerate)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is not supported for transactions with dust outputs.");
|
||||
}
|
||||
|
||||
mempool.PrioritiseTransaction(hash, nAmount);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -137,6 +137,219 @@ std::unique_ptr<CTxMemPool> MakeMempool(FuzzedDataProvider& fuzzed_data_provider
|
|||
return mempool;
|
||||
}
|
||||
|
||||
std::unique_ptr<CTxMemPool> MakeEphemeralMempool(const NodeContext& node)
|
||||
{
|
||||
// Take the default options for tests...
|
||||
CTxMemPool::Options mempool_opts{MemPoolOptionsForTest(node)};
|
||||
|
||||
mempool_opts.check_ratio = 1;
|
||||
|
||||
// Require standardness rules otherwise ephemeral dust is no-op
|
||||
mempool_opts.require_standard = true;
|
||||
|
||||
// And set minrelay to 0 to allow ephemeral parent tx even with non-TRUC
|
||||
mempool_opts.min_relay_feerate = CFeeRate(0);
|
||||
|
||||
bilingual_str error;
|
||||
// ...and construct a CTxMemPool from it
|
||||
auto mempool{std::make_unique<CTxMemPool>(std::move(mempool_opts), error)};
|
||||
Assert(error.empty());
|
||||
return mempool;
|
||||
}
|
||||
|
||||
// Scan mempool for a tx that has spent dust and return a
|
||||
// prevout of the child that isn't the dusty parent itself.
|
||||
// This is used to double-spend the child out of the mempool,
|
||||
// leaving the parent childless.
|
||||
// This assumes CheckMempoolEphemeralInvariants has passed for tx_pool.
|
||||
std::optional<COutPoint> GetChildEvictingPrevout(const CTxMemPool& tx_pool)
|
||||
{
|
||||
LOCK(tx_pool.cs);
|
||||
for (const auto& tx_info : tx_pool.infoAll()) {
|
||||
const auto& entry = *Assert(tx_pool.GetEntry(tx_info.tx->GetHash()));
|
||||
std::vector<uint32_t> dust_indexes{GetDustIndexes(tx_info.tx, tx_pool.m_opts.dust_relay_feerate)};
|
||||
if (!dust_indexes.empty()) {
|
||||
const auto& children = entry.GetMemPoolChildrenConst();
|
||||
if (!children.empty()) {
|
||||
Assert(children.size() == 1);
|
||||
// Find an input that doesn't spend from parent's txid
|
||||
const auto& only_child = children.begin()->get().GetTx();
|
||||
for (const auto& tx_input : only_child.vin) {
|
||||
if (tx_input.prevout.hash != tx_info.tx->GetHash()) {
|
||||
return tx_input.prevout;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FUZZ_TARGET(ephemeral_package_eval, .init = initialize_tx_pool)
|
||||
{
|
||||
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
|
||||
const auto& node = g_setup->m_node;
|
||||
auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
|
||||
|
||||
MockTime(fuzzed_data_provider, chainstate);
|
||||
|
||||
// All RBF-spendable outpoints outside of the unsubmitted package
|
||||
std::set<COutPoint> mempool_outpoints;
|
||||
std::map<COutPoint, CAmount> outpoints_value;
|
||||
for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
|
||||
Assert(mempool_outpoints.insert(outpoint).second);
|
||||
outpoints_value[outpoint] = 50 * COIN;
|
||||
}
|
||||
|
||||
auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints);
|
||||
node.validation_signals->RegisterSharedValidationInterface(outpoints_updater);
|
||||
|
||||
auto tx_pool_{MakeEphemeralMempool(node)};
|
||||
MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
|
||||
|
||||
chainstate.SetMempool(&tx_pool);
|
||||
|
||||
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
|
||||
{
|
||||
Assert(!mempool_outpoints.empty());
|
||||
|
||||
std::vector<CTransactionRef> txs;
|
||||
|
||||
// Find something we may want to double-spend with two input single tx
|
||||
std::optional<COutPoint> outpoint_to_rbf{GetChildEvictingPrevout(tx_pool)};
|
||||
bool should_rbf_eph_spend = outpoint_to_rbf && fuzzed_data_provider.ConsumeBool();
|
||||
|
||||
// Make small packages
|
||||
const auto num_txs = should_rbf_eph_spend ? 1 : (size_t) fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 4);
|
||||
|
||||
std::set<COutPoint> package_outpoints;
|
||||
while (txs.size() < num_txs) {
|
||||
|
||||
// Last transaction in a package needs to be a child of parents to get further in validation
|
||||
// so the last transaction to be generated(in a >1 package) must spend all package-made outputs
|
||||
// Note that this test currently only spends package outputs in last transaction.
|
||||
bool last_tx = num_txs > 1 && txs.size() == num_txs - 1;
|
||||
|
||||
// Create transaction to add to the mempool
|
||||
const CTransactionRef tx = [&] {
|
||||
CMutableTransaction tx_mut;
|
||||
tx_mut.version = CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nLockTime = 0;
|
||||
// Last tx will sweep half or more of all outpoints from package
|
||||
const auto num_in = should_rbf_eph_spend ? 2 :
|
||||
last_tx ? fuzzed_data_provider.ConsumeIntegralInRange<int>(package_outpoints.size()/2 + 1, package_outpoints.size()) :
|
||||
fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 4);
|
||||
auto num_out = should_rbf_eph_spend ? 1 : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 4);
|
||||
|
||||
auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
|
||||
|
||||
Assert((int)outpoints.size() >= num_in && num_in > 0);
|
||||
|
||||
CAmount amount_in{0};
|
||||
for (int i = 0; i < num_in; ++i) {
|
||||
// Pop random outpoint
|
||||
auto pop = outpoints.begin();
|
||||
std::advance(pop, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, outpoints.size() - 1));
|
||||
auto outpoint = *pop;
|
||||
|
||||
if (i == 0 && should_rbf_eph_spend) {
|
||||
outpoint = *outpoint_to_rbf;
|
||||
outpoints.erase(outpoint);
|
||||
} else {
|
||||
outpoints.erase(pop);
|
||||
}
|
||||
// no need to update or erase from outpoints_value
|
||||
amount_in += outpoints_value.at(outpoint);
|
||||
|
||||
// Create input
|
||||
CTxIn in;
|
||||
in.prevout = outpoint;
|
||||
in.scriptWitness.stack = P2WSH_EMPTY_TRUE_STACK;
|
||||
|
||||
tx_mut.vin.push_back(in);
|
||||
}
|
||||
|
||||
const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in);
|
||||
const auto amount_out = (amount_in - amount_fee) / num_out;
|
||||
for (int i = 0; i < num_out; ++i) {
|
||||
tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY);
|
||||
}
|
||||
|
||||
// Note output amounts can naturally drop to dust on their own.
|
||||
if (!should_rbf_eph_spend && fuzzed_data_provider.ConsumeBool()) {
|
||||
uint32_t dust_index = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, num_out);
|
||||
tx_mut.vout.insert(tx_mut.vout.begin() + dust_index, CTxOut(0, P2WSH_EMPTY));
|
||||
}
|
||||
|
||||
auto tx = MakeTransactionRef(tx_mut);
|
||||
// Restore previously removed outpoints, except in-package outpoints (to allow RBF)
|
||||
if (!last_tx) {
|
||||
for (const auto& in : tx->vin) {
|
||||
Assert(outpoints.insert(in.prevout).second);
|
||||
}
|
||||
// Cache the in-package outpoints being made
|
||||
for (size_t i = 0; i < tx->vout.size(); ++i) {
|
||||
package_outpoints.emplace(tx->GetHash(), i);
|
||||
}
|
||||
}
|
||||
// We need newly-created values for the duration of this run
|
||||
for (size_t i = 0; i < tx->vout.size(); ++i) {
|
||||
outpoints_value[COutPoint(tx->GetHash(), i)] = tx->vout[i].nValue;
|
||||
}
|
||||
return tx;
|
||||
}();
|
||||
txs.push_back(tx);
|
||||
}
|
||||
|
||||
if (fuzzed_data_provider.ConsumeBool()) {
|
||||
const auto& txid = fuzzed_data_provider.ConsumeBool() ?
|
||||
txs.back()->GetHash() :
|
||||
PickValue(fuzzed_data_provider, mempool_outpoints).hash;
|
||||
const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
|
||||
// We only prioritise out of mempool transactions since PrioritiseTransaction doesn't
|
||||
// filter for ephemeral dust GetEntry
|
||||
if (tx_pool.exists(GenTxid::Txid(txid))) {
|
||||
const auto tx_info{tx_pool.info(GenTxid::Txid(txid))};
|
||||
if (GetDustIndexes(tx_info.tx, tx_pool.m_opts.dust_relay_feerate).empty()) {
|
||||
tx_pool.PrioritiseTransaction(txid.ToUint256(), delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remember all added transactions
|
||||
std::set<CTransactionRef> added;
|
||||
auto txr = std::make_shared<TransactionsDelta>(added);
|
||||
node.validation_signals->RegisterSharedValidationInterface(txr);
|
||||
|
||||
auto single_submit = txs.size() == 1;
|
||||
|
||||
const auto result_package = WITH_LOCK(::cs_main,
|
||||
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*client_maxfeerate=*/{}));
|
||||
|
||||
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
|
||||
/*bypass_limits=*/fuzzed_data_provider.ConsumeBool(), /*test_accept=*/!single_submit));
|
||||
|
||||
if (!single_submit && result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
|
||||
// We don't know anything about the validity since transactions were randomly generated, so
|
||||
// just use result_package.m_state here. This makes the expect_valid check meaningless, but
|
||||
// we can still verify that the contents of m_tx_results are consistent with m_state.
|
||||
const bool expect_valid{result_package.m_state.IsValid()};
|
||||
Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, &tx_pool));
|
||||
}
|
||||
|
||||
node.validation_signals->SyncWithValidationInterfaceQueue();
|
||||
node.validation_signals->UnregisterSharedValidationInterface(txr);
|
||||
|
||||
CheckMempoolEphemeralInvariants(tx_pool);
|
||||
}
|
||||
|
||||
node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater);
|
||||
|
||||
WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
|
||||
}
|
||||
|
||||
|
||||
FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
||||
{
|
||||
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
|
||||
|
|
@ -325,6 +538,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
}
|
||||
|
||||
CheckMempoolTRUCInvariants(tx_pool);
|
||||
|
||||
// Dust checks only make sense when dust is enforced
|
||||
if (tx_pool.m_opts.require_standard) {
|
||||
CheckMempoolEphemeralInvariants(tx_pool);
|
||||
}
|
||||
}
|
||||
|
||||
node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater);
|
||||
|
|
|
|||
|
|
@ -830,6 +830,11 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
|
|||
// Check dust with default relay fee:
|
||||
CAmount nDustThreshold = 182 * g_dust.GetFeePerK() / 1000;
|
||||
BOOST_CHECK_EQUAL(nDustThreshold, 546);
|
||||
|
||||
// Add dust output to take dust slot, still standard!
|
||||
t.vout.emplace_back(t.vout[0].nAsset, CConfidentialValue(0), t.vout[0].scriptPubKey);
|
||||
CheckIsStandard(t);
|
||||
|
||||
// dust:
|
||||
t.vout[0].nValue = nDustThreshold - 1;
|
||||
CheckIsNotStandard(t, "dust");
|
||||
|
|
@ -986,6 +991,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
|
|||
CheckIsNotStandard(t, "bare-multisig");
|
||||
g_bare_multi = DEFAULT_PERMIT_BAREMULTISIG;
|
||||
|
||||
// Add dust output to take dust slot
|
||||
assert(t.vout.size() == 1);
|
||||
t.vout.emplace_back(t.vout[0].nAsset, CConfidentialValue(0), t.vout[0].scriptPubKey);
|
||||
|
||||
// Check compressed P2PK outputs dust threshold (must have leading 02 or 03)
|
||||
t.vout[0].scriptPubKey = CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG;
|
||||
t.vout[0].nValue = 576;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <key_io.h>
|
||||
#include <policy/packages.h>
|
||||
#include <policy/policy.h>
|
||||
#include <policy/ephemeral_policy.h>
|
||||
#include <policy/truc_policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
|
|
@ -89,6 +90,125 @@ static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, int3
|
|||
return MakeTransactionRef(mtx);
|
||||
}
|
||||
|
||||
// Same as make_tx but adds 2 normal outputs and 0-value dust to end of vout
|
||||
static inline CTransactionRef make_ephemeral_tx(const std::vector<COutPoint>& inputs, int32_t version)
|
||||
{
|
||||
CMutableTransaction mtx = CMutableTransaction{};
|
||||
mtx.version = version;
|
||||
mtx.vin.resize(inputs.size());
|
||||
mtx.vout.resize(3);
|
||||
for (size_t i{0}; i < inputs.size(); ++i) {
|
||||
mtx.vin[i].prevout = inputs[i];
|
||||
}
|
||||
for (auto i{0}; i < 3; ++i) {
|
||||
mtx.vout[i].scriptPubKey = CScript() << OP_TRUE;
|
||||
mtx.vout[i].nValue = (i == 2) ? 0 : 10000;
|
||||
}
|
||||
return MakeTransactionRef(mtx);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(ephemeral_tests, RegTestingSetup)
|
||||
{
|
||||
CTxMemPool& pool = *Assert(m_node.mempool);
|
||||
LOCK2(cs_main, pool.cs);
|
||||
TestMemPoolEntryHelper entry;
|
||||
CTxMemPool::setEntries empty_ancestors;
|
||||
|
||||
CFeeRate minrelay(1000);
|
||||
|
||||
// Basic transaction with dust
|
||||
auto grandparent_tx_1 = make_ephemeral_tx(random_outpoints(1), /*version=*/2);
|
||||
const auto dust_txid = grandparent_tx_1->GetHash();
|
||||
|
||||
uint32_t dust_index = 2;
|
||||
|
||||
// Child transaction spending dust
|
||||
auto dust_spend = make_tx({COutPoint{dust_txid, dust_index}}, /*version=*/2);
|
||||
|
||||
// We first start with nothing "in the mempool", using package checks
|
||||
|
||||
// Trivial single transaction with no dust
|
||||
BOOST_CHECK(!CheckEphemeralSpends({dust_spend}, minrelay, pool).has_value());
|
||||
|
||||
// Now with dust, ok because the tx has no dusty parents
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1}, minrelay, pool).has_value());
|
||||
|
||||
// Dust checks pass
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, dust_spend}, CFeeRate(0), pool).has_value());
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, dust_spend}, minrelay, pool).has_value());
|
||||
|
||||
auto dust_non_spend = make_tx({COutPoint{dust_txid, dust_index - 1}}, /*version=*/2);
|
||||
|
||||
// Child spending non-dust only from parent should be disallowed even if dust otherwise spent
|
||||
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_non_spend, dust_spend}, minrelay, pool).has_value());
|
||||
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_spend, dust_non_spend}, minrelay, pool).has_value());
|
||||
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_non_spend}, minrelay, pool).has_value());
|
||||
|
||||
auto grandparent_tx_2 = make_ephemeral_tx(random_outpoints(1), /*version=*/2);
|
||||
const auto dust_txid_2 = grandparent_tx_2->GetHash();
|
||||
|
||||
// Spend dust from one but not another is ok, as long as second grandparent has no child
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend}, minrelay, pool).has_value());
|
||||
|
||||
auto dust_non_spend_both_parents = make_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index - 1}}, /*version=*/2);
|
||||
// But if we spend from the parent, it must spend dust
|
||||
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_non_spend_both_parents}, minrelay, pool).has_value());
|
||||
|
||||
auto dust_spend_both_parents = make_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index}}, /*version=*/2);
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend_both_parents}, minrelay, pool).has_value());
|
||||
|
||||
// Spending other outputs is also correct, as long as the dusty one is spent
|
||||
const std::vector<COutPoint> all_outpoints{COutPoint(dust_txid, 0), COutPoint(dust_txid, 1), COutPoint(dust_txid, 2),
|
||||
COutPoint(dust_txid_2, 0), COutPoint(dust_txid_2, 1), COutPoint(dust_txid_2, 2)};
|
||||
auto dust_spend_all_outpoints = make_tx(all_outpoints, /*version=*/2);
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend_all_outpoints}, minrelay, pool).has_value());
|
||||
|
||||
// 2 grandparents with dust <- 1 dust-spending parent with dust <- child with no dust
|
||||
auto parent_with_dust = make_ephemeral_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index}}, /*version=*/2);
|
||||
// Ok for parent to have dust
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust}, minrelay, pool).has_value());
|
||||
auto child_no_dust = make_tx({COutPoint{parent_with_dust->GetHash(), dust_index}}, /*version=*/2);
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust, child_no_dust}, minrelay, pool).has_value());
|
||||
|
||||
// 2 grandparents with dust <- 1 dust-spending parent with dust <- child with dust
|
||||
auto child_with_dust = make_ephemeral_tx({COutPoint{parent_with_dust->GetHash(), dust_index}}, /*version=*/2);
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust, child_with_dust}, minrelay, pool).has_value());
|
||||
|
||||
// Tests with parents in mempool
|
||||
|
||||
// Nothing in mempool, this should pass for any transaction
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1}, minrelay, pool).has_value());
|
||||
|
||||
// Add first grandparent to mempool and fetch entry
|
||||
pool.addUnchecked(entry.FromTx(grandparent_tx_1));
|
||||
|
||||
// Ignores ancestors that aren't direct parents
|
||||
BOOST_CHECK(!CheckEphemeralSpends({child_no_dust}, minrelay, pool).has_value());
|
||||
|
||||
// Valid spend of dust with grandparent in mempool
|
||||
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust}, minrelay, pool).has_value());
|
||||
|
||||
// Second grandparent in same package
|
||||
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust, grandparent_tx_2}, minrelay, pool).has_value());
|
||||
// Order in package doesn't matter
|
||||
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_2, parent_with_dust}, minrelay, pool).has_value());
|
||||
|
||||
// Add second grandparent to mempool
|
||||
pool.addUnchecked(entry.FromTx(grandparent_tx_2));
|
||||
|
||||
// Only spends single dust out of two direct parents
|
||||
BOOST_CHECK(CheckEphemeralSpends({dust_non_spend_both_parents}, minrelay, pool).has_value());
|
||||
|
||||
// Spends both parents' dust
|
||||
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust}, minrelay, pool).has_value());
|
||||
|
||||
// Now add dusty parent to mempool
|
||||
pool.addUnchecked(entry.FromTx(parent_with_dust));
|
||||
|
||||
// Passes dust checks even with non-parent ancestors
|
||||
BOOST_CHECK(!CheckEphemeralSpends({child_no_dust}, minrelay, pool).has_value());
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(version3_tests, RegTestingSetup)
|
||||
{
|
||||
// Test TRUC policy helper functions
|
||||
|
|
|
|||
|
|
@ -142,6 +142,54 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> GetDustIndexes(const CTransactionRef& tx_ref, CFeeRate dust_relay_rate)
|
||||
{
|
||||
std::vector<uint32_t> dust_indexes;
|
||||
for (size_t i = 0; i < tx_ref->vout.size(); ++i) {
|
||||
const auto& output = tx_ref->vout[i];
|
||||
if (IsDust(output, dust_relay_rate)) dust_indexes.push_back(i);
|
||||
}
|
||||
|
||||
return dust_indexes;
|
||||
}
|
||||
|
||||
void CheckMempoolEphemeralInvariants(const CTxMemPool& tx_pool)
|
||||
{
|
||||
LOCK(tx_pool.cs);
|
||||
for (const auto& tx_info : tx_pool.infoAll()) {
|
||||
const auto& entry = *Assert(tx_pool.GetEntry(tx_info.tx->GetHash()));
|
||||
|
||||
std::vector<uint32_t> dust_indexes = GetDustIndexes(tx_info.tx, tx_pool.m_opts.dust_relay_feerate);
|
||||
|
||||
Assert(dust_indexes.size() < 2);
|
||||
|
||||
if (dust_indexes.empty()) continue;
|
||||
|
||||
// Transaction must have no base fee
|
||||
Assert(entry.GetFee() == 0 && entry.GetModifiedFee() == 0);
|
||||
|
||||
// Transaction has single dust; make sure it's swept or will not be mined
|
||||
const auto& children = entry.GetMemPoolChildrenConst();
|
||||
|
||||
// Multiple children should never happen as non-dust-spending child
|
||||
// can get mined as package
|
||||
Assert(children.size() < 2);
|
||||
|
||||
if (children.empty()) {
|
||||
// No children and no fees; modified fees aside won't get mined so it's fine
|
||||
// Happens naturally if child spend is RBF cycled away.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only-child should be spending the dust
|
||||
const auto& only_child = children.begin()->get().GetTx();
|
||||
COutPoint dust_outpoint{tx_info.tx->GetHash(), dust_indexes[0]};
|
||||
Assert(std::any_of(only_child.vin.begin(), only_child.vin.end(), [&dust_outpoint](const CTxIn& txin) {
|
||||
return txin.prevout == dust_outpoint;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckMempoolTRUCInvariants(const CTxMemPool& tx_pool)
|
||||
{
|
||||
LOCK(tx_pool.cs);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
|
|||
bool expect_valid,
|
||||
const CTxMemPool* mempool);
|
||||
|
||||
/** Check that we never get into a state where an ephemeral dust
|
||||
* transaction would be mined without the spend of the dust
|
||||
* also being mined. This assumes standardness checks are being
|
||||
* enforced.
|
||||
*/
|
||||
void CheckMempoolEphemeralInvariants(const CTxMemPool& tx_pool);
|
||||
|
||||
/** Return indexes of the transaction's outputs that are considered dust
|
||||
* at given dust_relay_rate.
|
||||
*/
|
||||
std::vector<uint32_t> GetDustIndexes(const CTransactionRef& tx_ref, CFeeRate dust_relay_rate);
|
||||
|
||||
/** For every transaction in tx_pool, check TRUC invariants:
|
||||
* - a TRUC tx's ancestor count must be within TRUC_ANCESTOR_LIMIT
|
||||
* - a TRUC tx's descendant count must be within TRUC_DESCENDANT_LIMIT
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
#include <node/blockstorage.h>
|
||||
#include <node/utxo_snapshot.h>
|
||||
#include <pegins.h>
|
||||
#include <policy/ephemeral_policy.h>
|
||||
#include <policy/policy.h>
|
||||
#include <policy/rbf.h>
|
||||
#include <policy/settings.h>
|
||||
|
|
@ -1134,6 +1135,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
|||
fSpendsCoinbase, nSigOpsCost, lock_points.value(), setPeginsSpent));
|
||||
ws.m_vsize = entry->GetTxSize();
|
||||
|
||||
// Enforces 0-fee for dust transactions, no incentive to be mined alone
|
||||
if (m_pool.m_opts.require_standard) {
|
||||
if (!CheckValidEphemeralTx(ptx, m_pool.m_opts.dust_relay_feerate, ws.m_base_fees, ws.m_modified_fees, state)) {
|
||||
return false; // state filled in by CheckValidEphemeralTx
|
||||
}
|
||||
}
|
||||
|
||||
if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST)
|
||||
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",
|
||||
strprintf("%d", nSigOpsCost));
|
||||
|
|
@ -1667,6 +1675,16 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
|
|||
return MempoolAcceptResult::Failure(ws.m_state);
|
||||
}
|
||||
|
||||
if (m_pool.m_opts.require_standard) {
|
||||
if (const auto ephemeral_violation{CheckEphemeralSpends(/*package=*/{ptx}, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
|
||||
const Txid& txid = ephemeral_violation.value();
|
||||
Assume(txid == ptx->GetHash());
|
||||
ws.m_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
|
||||
strprintf("tx %s did not spend parent's ephemeral dust", txid.ToString()));
|
||||
return MempoolAcceptResult::Failure(ws.m_state);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_subpackage.m_rbf && !ReplacementChecks(ws)) {
|
||||
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
|
||||
// Failed for incentives-based fee reasons. Provide the effective feerate and which tx was included.
|
||||
|
|
@ -1805,6 +1823,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
|
|||
return PackageMempoolAcceptResult(package_state, std::move(results));
|
||||
}
|
||||
|
||||
// Now that we've bounded the resulting possible ancestry count, check package for dust spends
|
||||
if (m_pool.m_opts.require_standard) {
|
||||
if (const auto ephemeral_violation{CheckEphemeralSpends(txns, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
|
||||
const Txid& child_txid = ephemeral_violation.value();
|
||||
TxValidationState child_state;
|
||||
child_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
|
||||
strprintf("tx %s did not spend parent's ephemeral dust", child_txid.ToString()));
|
||||
package_state.Invalid(PackageValidationResult::PCKG_TX, "unspent-dust");
|
||||
results.emplace(child_txid, MempoolAcceptResult::Failure(child_state));
|
||||
return PackageMempoolAcceptResult(package_state, std::move(results));
|
||||
}
|
||||
}
|
||||
|
||||
for (Workspace& ws : workspaces) {
|
||||
ws.m_package_feerate = package_feerate;
|
||||
if (!PolicyScriptChecks(args, ws)) {
|
||||
|
|
|
|||
|
|
@ -330,8 +330,10 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
|
|||
rawtxs=[tx.serialize().hex()],
|
||||
)
|
||||
tx = tx_from_hex(raw_tx_reference)
|
||||
original_vout0_value = tx.vout[0].nValue.getAmount() # ELEMENTS: save before replacing
|
||||
tx.vout[0] = output_p2sh_burn
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() - 1) # Make output smaller, such that it is dust for our policy
|
||||
tx.vout[1].nValue.setToAmount(tx.vout[1].nValue.getAmount() + original_vout0_value - tx.vout[0].nValue.getAmount()) # ELEMENTS: balance tx via fee
|
||||
self.check_mempool_result(
|
||||
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'dust'}],
|
||||
rawtxs=[tx.serialize().hex()],
|
||||
|
|
|
|||
|
|
@ -65,18 +65,50 @@ class DustRelayFeeTest(BitcoinTestFramework):
|
|||
assert_equal(res['allowed'], True)
|
||||
|
||||
# amount just below the dust threshold should fail
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() + 1)
|
||||
tx.vout[2].nValue.setToAmount(tx.vout[2].nValue.getAmount() - 1)
|
||||
res = node.testmempoolaccept([tx.serialize().hex()])[0]
|
||||
assert_equal(res['allowed'], False)
|
||||
assert_equal(res['reject-reason'], 'dust')
|
||||
# ELEMENTS: skip when threshold is 1, since threshold-1=0 is an invalid output value
|
||||
if dust_threshold > 1:
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() + 1)
|
||||
tx.vout[2].nValue.setToAmount(tx.vout[2].nValue.getAmount() - 1)
|
||||
res = node.testmempoolaccept([tx.serialize().hex()])[0]
|
||||
assert_equal(res['allowed'], False)
|
||||
assert_equal(res['reject-reason'], 'dust')
|
||||
|
||||
# finally send the transaction to avoid running out of MiniWallet UTXOs
|
||||
self.wallet.sendrawtransaction(from_node=node, tx_hex=tx_good_hex)
|
||||
|
||||
def test_dustrelay(self):
|
||||
self.log.info("Test that small outputs are acceptable when dust relay rate is set to 0 that would otherwise trigger ephemeral dust rules")
|
||||
|
||||
self.restart_node(0, extra_args=["-dustrelayfee=0"])
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# Double dust, both unspent, with fees. Would have failed individual checks.
|
||||
# Dust is 1 satoshi create_self_transfer_multi disallows 0
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=1000, amount_per_output=1, num_outputs=2)
|
||||
dust_txid = self.nodes[0].sendrawtransaction(hexstring=dusty_tx["hex"], maxfeerate=0)
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [dust_txid])
|
||||
|
||||
# Spends one dust along with fee input, leave other dust unspent to check ephemeral dust checks aren't being enforced
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[self.wallet.get_utxo(), dusty_tx["new_utxos"][0]])
|
||||
sweep_txid = self.nodes[0].sendrawtransaction(sweep_tx["hex"])
|
||||
|
||||
mempool_entries = self.nodes[0].getrawmempool()
|
||||
assert dust_txid in mempool_entries
|
||||
assert sweep_txid in mempool_entries
|
||||
assert_equal(len(mempool_entries), 2)
|
||||
|
||||
# Wipe extra arg to reset dust relay
|
||||
self.restart_node(0, extra_args=[])
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
def run_test(self):
|
||||
self.wallet = MiniWallet(self.nodes[0])
|
||||
|
||||
self.test_dustrelay()
|
||||
|
||||
# prepare output scripts of each standard type
|
||||
_, uncompressed_pubkey = generate_keypair(compressed=False)
|
||||
_, pubkey = generate_keypair(compressed=True)
|
||||
|
|
@ -100,10 +132,12 @@ class DustRelayFeeTest(BitcoinTestFramework):
|
|||
# test default (no parameter), disabled (=0) and a bunch of arbitrary dust fee rates [sat/kvB]
|
||||
for dustfee_sat_kvb in (DUST_RELAY_TX_FEE, 0, 1, 66, 500, 1337, 12345, 21212, 333333):
|
||||
dustfee_btc_kvb = dustfee_sat_kvb / Decimal(COIN)
|
||||
dust_parameter = f"-dustrelayfee={dustfee_btc_kvb:.8f}"
|
||||
if dustfee_sat_kvb == DUST_RELAY_TX_FEE:
|
||||
self.log.info(f"Test default dust limit setting ({dustfee_sat_kvb} sat/kvB)...")
|
||||
# ELEMENTS: test_dustrelay() restarts node with [], losing -dustrelayfee; restore it here
|
||||
self.restart_node(0, extra_args=[dust_parameter, "-permitbaremultisig"])
|
||||
else:
|
||||
dust_parameter = f"-dustrelayfee={dustfee_btc_kvb:.8f}"
|
||||
self.log.info(f"Test dust limit setting {dust_parameter} ({dustfee_sat_kvb} sat/kvB)...")
|
||||
self.restart_node(0, extra_args=[dust_parameter, "-permitbaremultisig"])
|
||||
|
||||
|
|
|
|||
483
test/functional/mempool_ephemeral_dust.py
Executable file
483
test/functional/mempool_ephemeral_dust.py
Executable file
|
|
@ -0,0 +1,483 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2024-present The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from test_framework.messages import (
|
||||
COIN,
|
||||
CTxOut,
|
||||
)
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.mempool_util import assert_mempool_contents
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
from test_framework.wallet import (
|
||||
MiniWallet,
|
||||
)
|
||||
|
||||
class EphemeralDustTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
# Mempools should match via 1P1C p2p relay
|
||||
self.num_nodes = 2
|
||||
|
||||
# Don't test trickling logic
|
||||
self.noban_tx_relay = True
|
||||
|
||||
def add_output_to_create_multi_result(self, result, output_value=1): # ELEMENTS: 0-value spendable outputs are invalid
|
||||
""" Add output without changing absolute tx fee
|
||||
"""
|
||||
assert len(result["tx"].vout) > 0
|
||||
assert result["tx"].vout[0].nValue.getAmount() >= output_value
|
||||
result["tx"].vout.append(CTxOut(output_value, result["tx"].vout[0].scriptPubKey))
|
||||
# Take value from first output
|
||||
result["tx"].vout[0].nValue.setToAmount(result["tx"].vout[0].nValue.getAmount() - output_value)
|
||||
result["new_utxos"][0]["value"] = Decimal(result["tx"].vout[0].nValue.getAmount()) / COIN
|
||||
new_txid = result["tx"].rehash()
|
||||
result["txid"] = new_txid
|
||||
result["wtxid"] = result["tx"].getwtxid()
|
||||
result["hex"] = result["tx"].serialize().hex()
|
||||
for new_utxo in result["new_utxos"]:
|
||||
new_utxo["txid"] = new_txid
|
||||
new_utxo["wtxid"] = result["tx"].getwtxid()
|
||||
|
||||
result["new_utxos"].append({"txid": new_txid, "vout": len(result["tx"].vout) - 1, "value": Decimal(output_value) / COIN, "height": 0, "coinbase": False, "confirmations": 0})
|
||||
|
||||
def run_test(self):
|
||||
|
||||
node = self.nodes[0]
|
||||
self.wallet = MiniWallet(node)
|
||||
|
||||
self.test_normal_dust()
|
||||
self.test_sponsor_cycle()
|
||||
self.test_node_restart()
|
||||
self.test_fee_having_parent()
|
||||
self.test_multidust()
|
||||
self.test_nonzero_dust()
|
||||
self.test_non_truc()
|
||||
self.test_unspent_ephemeral()
|
||||
self.test_reorgs()
|
||||
self.test_free_relay()
|
||||
|
||||
def test_normal_dust(self):
|
||||
self.log.info("Create 0-value dusty output, show that it works inside truc when spent in package")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
# Test doesn't work because lack of package feerates
|
||||
test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert not test_res[0]["allowed"]
|
||||
assert_equal(test_res[0]["reject-reason"], "min relay fee not met")
|
||||
|
||||
# And doesn't work on its own
|
||||
assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])
|
||||
|
||||
# If we add modified fees, it is still not allowed due to dust check
|
||||
self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=COIN)
|
||||
test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
|
||||
assert not test_res[0]["allowed"]
|
||||
assert_equal(test_res[0]["reject-reason"], "dust")
|
||||
# Reset priority
|
||||
self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-COIN)
|
||||
assert_equal(self.nodes[0].getprioritisedtransactions(), {})
|
||||
|
||||
# Package evaluation succeeds
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "success")
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# Entry is denied when non-0-fee, either base or unmodified.
|
||||
# If in-mempool, we're not allowed to prioritise due to detected dust output
|
||||
assert_raises_rpc_error(-8, "Priority is not supported for transactions with dust outputs.", self.nodes[0].prioritisetransaction, dusty_tx["txid"], 0, 1)
|
||||
assert_equal(self.nodes[0].getprioritisedtransactions(), {})
|
||||
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
def test_node_restart(self):
|
||||
self.log.info("Test that an ephemeral package is rejected on restart due to individual evaluation")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "success")
|
||||
assert_equal(len(self.nodes[0].getrawmempool()), 2)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# Node restart; doesn't allow allow ephemeral transaction back in due to individual submission
|
||||
# resulting in 0-fee. Supporting re-submission of CPFP packages on restart is desired but not
|
||||
# yet implemented.
|
||||
self.restart_node(0)
|
||||
self.restart_node(1)
|
||||
self.connect_nodes(0, 1)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
def test_fee_having_parent(self):
|
||||
self.log.info("Test that a transaction with ephemeral dust may not have non-0 base fee")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
sats_fee = 1
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=sats_fee, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
assert_equal(int(COIN * dusty_tx["fee"]), sats_fee) # has fees
|
||||
assert_greater_than(dusty_tx["tx"].vout[0].nValue.getAmount(), 330) # main output is not dust
|
||||
assert_equal(dusty_tx["tx"].vout[-1].nValue.getAmount(), 1) # added one is dust (1 sat, below threshold)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
# When base fee is non-0, we report dust like usual
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
|
||||
|
||||
# Priority is ignored: rejected even if modified fee is 0
|
||||
self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)
|
||||
self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
|
||||
|
||||
# Will not be accepted if base fee is 0 with modified fee of non-0
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)
|
||||
self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)
|
||||
|
||||
# It's rejected submitted alone
|
||||
test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
|
||||
assert not test_res[0]["allowed"]
|
||||
assert_equal(test_res[0]["reject-reason"], "dust")
|
||||
|
||||
# Or as a package
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
|
||||
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
def test_multidust(self):
|
||||
self.log.info("Test that a transaction with multiple ephemeral dusts is not allowed")
|
||||
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust")
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
def test_nonzero_dust(self):
|
||||
self.log.info("Test that a single output of any satoshi amount is allowed, not checking spending")
|
||||
|
||||
# We aren't checking spending, allow it in with no fee
|
||||
self.restart_node(0, extra_args=["-minrelaytxfee=0"])
|
||||
self.restart_node(1, extra_args=["-minrelaytxfee=0"])
|
||||
self.connect_nodes(0, 1)
|
||||
|
||||
# 330 is dust threshold for taproot outputs
|
||||
for value in [1, 329, 330]:
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx, value)
|
||||
|
||||
test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
|
||||
assert test_res[0]["allowed"]
|
||||
|
||||
self.restart_node(0, extra_args=[])
|
||||
self.restart_node(1, extra_args=[])
|
||||
self.connect_nodes(0, 1)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
# N.B. If individual minrelay requirement is dropped, this test can be dropped
|
||||
def test_non_truc(self):
|
||||
self.log.info("Test that v2 dust-having transaction is rejected even if spent, because of min relay requirement")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=2)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=2)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "transaction failed")
|
||||
# ELEMENTS: tx vsize is larger due to nAsset/nNonce overhead; compute dynamically
|
||||
assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], f"min relay fee not met, 0 < {dusty_tx['tx'].get_vsize()}")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
def test_unspent_ephemeral(self):
|
||||
self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx, 10) # ELEMENTS: dust threshold +-15 sats; 10 < 15
|
||||
|
||||
# Valid sweep we will RBF incorrectly by not spending dust as well
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# Doesn't spend in-mempool dust output from parent
|
||||
unspent_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
|
||||
assert_greater_than(unspent_sweep_tx["fee"], sweep_tx["fee"])
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])
|
||||
assert_equal(res["tx-results"][unspent_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {unspent_sweep_tx['txid']} did not spend parent's ephemeral dust")
|
||||
assert_raises_rpc_error(-26, f"missing-ephemeral-spends, tx {unspent_sweep_tx['txid']} did not spend parent's ephemeral dust", self.nodes[0].sendrawtransaction, unspent_sweep_tx["hex"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# Spend works with dust spent
|
||||
sweep_tx_2 = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
assert sweep_tx["hex"] != sweep_tx_2["hex"]
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx_2["hex"]])
|
||||
assert_equal(res["package_msg"], "success")
|
||||
|
||||
# Re-set and test again with nothing from package in mempool this time
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx, 10) # ELEMENTS: dust threshold +-15 sats; 10 < 15
|
||||
|
||||
# Spend non-dust only
|
||||
unspent_sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "unspent-dust")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# Now spend dust only which should work
|
||||
second_coin = self.wallet.get_utxo() # another fee-bringing coin
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][1], second_coin], version=3)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "success")
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
def test_sponsor_cycle(self):
|
||||
self.log.info("Test that dust txn is not evicted when it becomes childless, but won't be mined")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(
|
||||
fee_per_output=0,
|
||||
version=3
|
||||
)
|
||||
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sponsor_coin = self.wallet.get_utxo()
|
||||
|
||||
# Bring "fee" input that can be double-spend separately
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"] + [sponsor_coin], version=3)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
assert_equal(res["package_msg"], "success")
|
||||
assert_equal(len(self.nodes[0].getrawmempool()), 2)
|
||||
# sync to make sure unsponsor_tx hits second node's mempool after initial package
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# Now we RBF away the child using the sponsor input only
|
||||
unsponsor_tx = self.wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[sponsor_coin],
|
||||
num_outputs=1,
|
||||
fee_per_output=2000,
|
||||
version=3
|
||||
)
|
||||
self.nodes[0].sendrawtransaction(unsponsor_tx["hex"])
|
||||
|
||||
# Parent is now childless and fee-free, so will not be mined
|
||||
entry_info = self.nodes[0].getmempoolentry(dusty_tx["txid"])
|
||||
assert_equal(entry_info["descendantcount"], 1)
|
||||
assert_equal(entry_info["fees"]["descendant"], Decimal(0))
|
||||
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], unsponsor_tx["tx"]])
|
||||
|
||||
# Dust tx is not mined
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]])
|
||||
|
||||
# Create sweep that doesn't spend conflicting sponsor coin
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
|
||||
# Can resweep
|
||||
self.nodes[0].sendrawtransaction(sweep_tx["hex"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[])
|
||||
|
||||
def test_reorgs(self):
|
||||
self.log.info("Test that reorgs breaking the truc topology doesn't cause issues")
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# Many shallow re-orgs confuse block gossiping making test less reliable otherwise
|
||||
self.disconnect_nodes(0, 1)
|
||||
|
||||
# Get dusty tx mined, then check that it makes it back into mempool on reorg
|
||||
# due to bypass_limits allowing 0-fee individually
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])
|
||||
|
||||
block_res = self.nodes[0].rpc.generateblock(self.wallet.get_address(), [dusty_tx["hex"]])
|
||||
self.nodes[0].invalidateblock(block_res["hash"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]], sync=False)
|
||||
|
||||
# Create a sweep that has dust of its own and leaves dusty_tx's dust unspent
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
|
||||
self.add_output_to_create_multi_result(sweep_tx)
|
||||
assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, sweep_tx["hex"])
|
||||
|
||||
# Mine the sweep then re-org, the sweep will not make it back in due to spend checks
|
||||
block_res = self.nodes[0].rpc.generateblock(self.wallet.get_address(), [dusty_tx["hex"], sweep_tx["hex"]])
|
||||
self.nodes[0].invalidateblock(block_res["hash"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]], sync=False)
|
||||
|
||||
# Also should happen if dust is swept
|
||||
sweep_tx_2 = self.wallet.create_self_transfer_multi(fee_per_output=0, utxos_to_spend=dusty_tx["new_utxos"], version=3)
|
||||
self.add_output_to_create_multi_result(sweep_tx_2)
|
||||
assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, sweep_tx_2["hex"])
|
||||
|
||||
reconsider_block_res = self.nodes[0].rpc.generateblock(self.wallet.get_address(), [dusty_tx["hex"], sweep_tx_2["hex"]])
|
||||
self.nodes[0].invalidateblock(reconsider_block_res["hash"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx_2["tx"]], sync=False)
|
||||
|
||||
# TRUC transactions restriction for ephemeral dust disallows further spends of ancestor chains
|
||||
child_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=sweep_tx_2["new_utxos"], version=3)
|
||||
assert_raises_rpc_error(-26, "TRUC-violation", self.nodes[0].sendrawtransaction, child_tx["hex"])
|
||||
|
||||
self.nodes[0].reconsiderblock(reconsider_block_res["hash"])
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
self.log.info("Test that ephemeral dust tx with fees or multi dust don't enter mempool via reorg")
|
||||
multi_dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=3)
|
||||
self.add_output_to_create_multi_result(multi_dusty_tx)
|
||||
self.add_output_to_create_multi_result(multi_dusty_tx)
|
||||
|
||||
block_res = self.nodes[0].rpc.generateblock(self.wallet.get_address(), [multi_dusty_tx["hex"]])
|
||||
self.nodes[0].invalidateblock(block_res["hash"])
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# With fee and one dust
|
||||
dusty_fee_tx = self.wallet.create_self_transfer_multi(fee_per_output=1, version=3)
|
||||
self.add_output_to_create_multi_result(dusty_fee_tx)
|
||||
|
||||
block_res = self.nodes[0].rpc.generateblock(self.wallet.get_address(), [dusty_fee_tx["hex"]])
|
||||
self.nodes[0].invalidateblock(block_res["hash"])
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# Re-connect and make sure we have same state still
|
||||
self.connect_nodes(0, 1)
|
||||
self.sync_all()
|
||||
|
||||
# N.B. this extra_args can be removed post cluster mempool
|
||||
def test_free_relay(self):
|
||||
self.log.info("Test that ephemeral dust works in non-TRUC contexts when there's no minrelay requirement")
|
||||
|
||||
# Note: since minrelay is 0, it is not testing 1P1C relay
|
||||
self.restart_node(0, extra_args=["-minrelaytxfee=0"])
|
||||
self.restart_node(1, extra_args=["-minrelaytxfee=0"])
|
||||
self.connect_nodes(0, 1)
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, version=2)
|
||||
self.add_output_to_create_multi_result(dusty_tx)
|
||||
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=2)
|
||||
|
||||
self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
|
||||
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
|
||||
|
||||
# generate coins for next tests
|
||||
self.generate(self.nodes[0], 1)
|
||||
self.wallet.rescan_utxos()
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
self.log.info("Test batched ephemeral dust sweep")
|
||||
dusty_txs = []
|
||||
for _ in range(24):
|
||||
dusty_txs.append(self.wallet.create_self_transfer_multi(fee_per_output=0, version=2))
|
||||
self.add_output_to_create_multi_result(dusty_txs[-1])
|
||||
|
||||
all_parent_utxos = [utxo for tx in dusty_txs for utxo in tx["new_utxos"]]
|
||||
|
||||
# Missing one dust spend from a single parent, child rejected
|
||||
insufficient_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos[:-1], version=2)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])
|
||||
assert_equal(res['package_msg'], "transaction failed")
|
||||
assert_equal(res['tx-results'][insufficient_sweep_tx['wtxid']]['error'], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} did not spend parent's ephemeral dust")
|
||||
# Everything got in except for insufficient spend
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs])
|
||||
|
||||
# Next put some parents in mempool, but not others, and test unspent dust again with all parents spent
|
||||
B_coin = self.wallet.get_utxo() # coin to cycle out CPFP
|
||||
sweep_all_but_one_tx = self.wallet.create_self_transfer_multi(fee_per_output=20000, utxos_to_spend=all_parent_utxos[:-2] + [B_coin], version=2)
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs[:-1]] + [sweep_all_but_one_tx["hex"]])
|
||||
assert_equal(res['package_msg'], "success")
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])
|
||||
assert_equal(res['package_msg'], "transaction failed")
|
||||
assert_equal(res['tx-results'][insufficient_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} did not spend parent's ephemeral dust")
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])
|
||||
|
||||
# Cycle out the partial sweep to avoid triggering package RBF behavior which limits package to no in-mempool ancestors
|
||||
cancel_sweep = self.wallet.create_self_transfer_multi(fee_per_output=21000, utxos_to_spend=[B_coin], version=2)
|
||||
self.nodes[0].sendrawtransaction(cancel_sweep["hex"])
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [cancel_sweep["tx"]])
|
||||
|
||||
# Sweeps all dust, where all dusty txs are already in-mempool
|
||||
sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos, version=2)
|
||||
|
||||
res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [sweep_tx["hex"]])
|
||||
assert_equal(res['package_msg'], "success")
|
||||
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_tx["tx"], cancel_sweep["tx"]])
|
||||
|
||||
self.generate(self.nodes[0], 25)
|
||||
self.wallet.rescan_utxos()
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
# Other topology tests require relaxation of submitpackage topology
|
||||
|
||||
self.restart_node(0, extra_args=[])
|
||||
self.restart_node(1, extra_args=[])
|
||||
self.connect_nodes(0, 1)
|
||||
|
||||
assert_equal(self.nodes[0].getrawmempool(), [])
|
||||
|
||||
if __name__ == "__main__":
|
||||
EphemeralDustTest(__file__).main()
|
||||
|
|
@ -21,6 +21,20 @@ from .wallet import (
|
|||
|
||||
ORPHAN_TX_EXPIRE_TIME = 1200
|
||||
|
||||
def assert_mempool_contents(test_framework, node, expected=None, sync=True):
|
||||
"""Assert that all transactions in expected are in the mempool,
|
||||
and no additional ones exist. 'expected' is an array of
|
||||
CTransaction objects
|
||||
"""
|
||||
if sync:
|
||||
test_framework.sync_mempools()
|
||||
if not expected:
|
||||
expected = []
|
||||
mempool = node.getrawmempool(verbose=False)
|
||||
assert_equal(len(mempool), len(expected))
|
||||
for tx in expected:
|
||||
assert tx.rehash() in mempool
|
||||
|
||||
|
||||
def fill_mempool(test_framework, node, *, tx_sync_fun=None):
|
||||
"""Fill mempool until eviction.
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ class MiniWallet:
|
|||
coinbase=False,
|
||||
confirmations=0,
|
||||
) for i in range(len(tx.vout) - ( 1 if fee else 0 ))], # ELEMENTS remove fee output only if fee is non-zero
|
||||
"fee": fee,
|
||||
"fee": Decimal(fee) / COIN, # ELEMENTS: fee is in sats here, convert to BTC
|
||||
"txid": txid,
|
||||
"wtxid": tx.getwtxid(),
|
||||
"hex": tx.serialize().hex(),
|
||||
|
|
|
|||
|
|
@ -449,6 +449,7 @@ BASE_SCRIPTS = [
|
|||
'rpc_getdescriptorinfo.py',
|
||||
'rpc_mempool_info.py',
|
||||
'rpc_help.py',
|
||||
'mempool_ephemeral_dust.py',
|
||||
'p2p_handshake.py',
|
||||
'p2p_handshake.py --v2transport',
|
||||
'feature_dirsymlinks.py',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue