mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-15 12:51:00 +02:00
Merge 7143d43884 into merged_master (Bitcoin PR bitcoin/bitcoin#28948)
This commit is contained in:
commit
474dd0f6f9
24 changed files with 1238 additions and 142 deletions
|
|
@ -11,7 +11,8 @@ their in-mempool descendants (together, "original transactions") if, in addition
|
|||
other consensus and policy rules, each of the following conditions are met:
|
||||
|
||||
1. The directly conflicting transactions all signal replaceability explicitly. A transaction is
|
||||
signaling replaceability if any of its inputs have an nSequence number less than (0xffffffff - 1).
|
||||
signaling BIP125 replaceability if any of its inputs have an nSequence number less than (0xffffffff - 1).
|
||||
A transaction also signals replaceibility if its nVersion field is set to 3.
|
||||
|
||||
*Rationale*: See [BIP125
|
||||
explanation](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki#motivation).
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ BITCOIN_CORE_H = \
|
|||
outputtype.h \
|
||||
pegins.h \
|
||||
policy/discount.h \
|
||||
policy/v3_policy.h \
|
||||
policy/feerate.h \
|
||||
policy/fees.h \
|
||||
policy/fees_args.h \
|
||||
|
|
@ -461,6 +462,7 @@ libbitcoin_node_a_SOURCES = \
|
|||
node/validation_cache_args.cpp \
|
||||
noui.cpp \
|
||||
pegins.cpp \
|
||||
policy/v3_policy.cpp \
|
||||
policy/fees.cpp \
|
||||
policy/fees_args.cpp \
|
||||
policy/packages.cpp \
|
||||
|
|
@ -745,6 +747,7 @@ libbitcoin_common_a_SOURCES = \
|
|||
netbase.cpp \
|
||||
net_permissions.cpp \
|
||||
outputtype.cpp \
|
||||
policy/v3_policy.cpp \
|
||||
policy/feerate.cpp \
|
||||
policy/policy.cpp \
|
||||
protocol.cpp \
|
||||
|
|
@ -1043,6 +1046,7 @@ libbitcoinkernel_la_SOURCES = \
|
|||
node/blockstorage.cpp \
|
||||
node/chainstate.cpp \
|
||||
node/utxo_snapshot.cpp \
|
||||
policy/v3_policy.cpp \
|
||||
policy/feerate.cpp \
|
||||
policy/packages.cpp \
|
||||
policy/policy.cpp \
|
||||
|
|
|
|||
|
|
@ -115,11 +115,11 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx,
|
|||
}
|
||||
|
||||
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
|
||||
const std::set<uint256>& direct_conflicts,
|
||||
const std::set<Txid>& direct_conflicts,
|
||||
const uint256& txid)
|
||||
{
|
||||
for (CTxMemPool::txiter ancestorIt : ancestors) {
|
||||
const uint256& hashAncestor = ancestorIt->GetTx().GetHash();
|
||||
const Txid& hashAncestor = ancestorIt->GetTx().GetHash();
|
||||
if (direct_conflicts.count(hashAncestor)) {
|
||||
return strprintf("%s spends conflicting transaction %s",
|
||||
txid.ToString(),
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx, const CTx
|
|||
* @returns error message if the sets intersect, std::nullopt if they are disjoint.
|
||||
*/
|
||||
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
|
||||
const std::set<uint256>& direct_conflicts,
|
||||
const std::set<Txid>& direct_conflicts,
|
||||
const uint256& txid);
|
||||
|
||||
/** Check that the feerate of the replacement transaction(s) is higher than the feerate of each
|
||||
|
|
|
|||
220
src/policy/v3_policy.cpp
Normal file
220
src/policy/v3_policy.cpp
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Copyright (c) 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 <policy/v3_policy.h>
|
||||
|
||||
#include <coins.h>
|
||||
#include <consensus/amount.h>
|
||||
#include <logging.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/check.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
/** Helper for PackageV3Checks: Returns a vector containing the indices of transactions (within
|
||||
* package) that are direct parents of ptx. */
|
||||
std::vector<size_t> FindInPackageParents(const Package& package, const CTransactionRef& ptx)
|
||||
{
|
||||
std::vector<size_t> in_package_parents;
|
||||
|
||||
std::set<Txid> possible_parents;
|
||||
for (auto &input : ptx->vin) {
|
||||
possible_parents.insert(input.prevout.hash);
|
||||
}
|
||||
|
||||
for (size_t i{0}; i < package.size(); ++i) {
|
||||
const auto& tx = package.at(i);
|
||||
// We assume the package is sorted, so that we don't need to continue
|
||||
// looking past the transaction itself.
|
||||
if (&(*tx) == &(*ptx)) break;
|
||||
if (possible_parents.count(tx->GetHash())) {
|
||||
in_package_parents.push_back(i);
|
||||
}
|
||||
}
|
||||
return in_package_parents;
|
||||
}
|
||||
|
||||
/** Helper for PackageV3Checks, storing info for a mempool or package parent. */
|
||||
struct ParentInfo {
|
||||
/** Txid used to identify this parent by prevout */
|
||||
const Txid& m_txid;
|
||||
/** Wtxid used for debug string */
|
||||
const Wtxid& m_wtxid;
|
||||
/** nVersion used to check inheritance of v3 and non-v3 */
|
||||
decltype(CTransaction::nVersion) m_version;
|
||||
/** If parent is in mempool, whether it has any descendants in mempool. */
|
||||
bool m_has_mempool_descendant;
|
||||
|
||||
ParentInfo() = delete;
|
||||
ParentInfo(const Txid& txid, const Wtxid& wtxid, decltype(CTransaction::nVersion) version, bool has_mempool_descendant) :
|
||||
m_txid{txid}, m_wtxid{wtxid}, m_version{version},
|
||||
m_has_mempool_descendant{has_mempool_descendant}
|
||||
{}
|
||||
};
|
||||
|
||||
std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t vsize,
|
||||
const Package& package,
|
||||
const CTxMemPool::setEntries& mempool_ancestors)
|
||||
{
|
||||
// This function is specialized for these limits, and must be reimplemented if they ever change.
|
||||
static_assert(V3_ANCESTOR_LIMIT == 2);
|
||||
static_assert(V3_DESCENDANT_LIMIT == 2);
|
||||
|
||||
const auto in_package_parents{FindInPackageParents(package, ptx)};
|
||||
|
||||
// Now we have all ancestors, so we can start checking v3 rules.
|
||||
if (ptx->nVersion == 3) {
|
||||
if (mempool_ancestors.size() + in_package_parents.size() + 1 > V3_ANCESTOR_LIMIT) {
|
||||
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString());
|
||||
}
|
||||
|
||||
const bool has_parent{mempool_ancestors.size() + in_package_parents.size() > 0};
|
||||
if (has_parent) {
|
||||
// A v3 child cannot be too large.
|
||||
if (vsize > V3_CHILD_MAX_VSIZE) {
|
||||
return strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
|
||||
vsize, V3_CHILD_MAX_VSIZE);
|
||||
}
|
||||
|
||||
const auto parent_info = [&] {
|
||||
if (mempool_ancestors.size() > 0) {
|
||||
// There's a parent in the mempool.
|
||||
auto& mempool_parent = *mempool_ancestors.begin();
|
||||
Assume(mempool_parent->GetCountWithDescendants() == 1);
|
||||
return ParentInfo{mempool_parent->GetTx().GetHash(),
|
||||
mempool_parent->GetTx().GetWitnessHash(),
|
||||
mempool_parent->GetTx().nVersion,
|
||||
/*has_mempool_descendant=*/mempool_parent->GetCountWithDescendants() > 1};
|
||||
} else {
|
||||
// Ancestor must be in the package. Find it.
|
||||
auto& parent_index = in_package_parents.front();
|
||||
auto& package_parent = package.at(parent_index);
|
||||
return ParentInfo{package_parent->GetHash(),
|
||||
package_parent->GetWitnessHash(),
|
||||
package_parent->nVersion,
|
||||
/*has_mempool_descendant=*/false};
|
||||
}
|
||||
}();
|
||||
|
||||
// If there is a parent, it must have the right version.
|
||||
if (parent_info.m_version != 3) {
|
||||
return strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
|
||||
parent_info.m_txid.ToString(), parent_info.m_wtxid.ToString());
|
||||
}
|
||||
|
||||
for (const auto& package_tx : package) {
|
||||
// Skip same tx.
|
||||
if (&(*package_tx) == &(*ptx)) continue;
|
||||
|
||||
for (auto& input : package_tx->vin) {
|
||||
// Fail if we find another tx with the same parent. We don't check whether the
|
||||
// sibling is to-be-replaced (done in SingleV3Checks) because these transactions
|
||||
// are within the same package.
|
||||
if (input.prevout.hash == parent_info.m_txid) {
|
||||
return strprintf("tx %s (wtxid=%s) would exceed descendant count limit",
|
||||
parent_info.m_txid.ToString(),
|
||||
parent_info.m_wtxid.ToString());
|
||||
}
|
||||
|
||||
// This tx can't have both a parent and an in-package child.
|
||||
if (input.prevout.hash == ptx->GetHash()) {
|
||||
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
package_tx->GetHash().ToString(), package_tx->GetWitnessHash().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It shouldn't be possible to have any mempool siblings at this point. SingleV3Checks
|
||||
// catches mempool siblings. Also, if the package consists of connected transactions,
|
||||
// any tx having a mempool ancestor would mean the package exceeds ancestor limits.
|
||||
if (!Assume(!parent_info.m_has_mempool_descendant)) {
|
||||
return strprintf("tx %u would exceed descendant count limit", parent_info.m_wtxid.ToString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-v3 transactions cannot have v3 parents.
|
||||
for (auto it : mempool_ancestors) {
|
||||
if (it->GetTx().nVersion == 3) {
|
||||
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
|
||||
it->GetSharedTx()->GetHash().ToString(), it->GetSharedTx()->GetWitnessHash().ToString());
|
||||
}
|
||||
}
|
||||
for (const auto& index: in_package_parents) {
|
||||
if (package.at(index)->nVersion == 3) {
|
||||
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
|
||||
ptx->GetHash().ToString(),
|
||||
ptx->GetWitnessHash().ToString(),
|
||||
package.at(index)->GetHash().ToString(),
|
||||
package.at(index)->GetWitnessHash().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> SingleV3Checks(const CTransactionRef& ptx,
|
||||
const CTxMemPool::setEntries& mempool_ancestors,
|
||||
const std::set<Txid>& direct_conflicts,
|
||||
int64_t vsize)
|
||||
{
|
||||
// Check v3 and non-v3 inheritance.
|
||||
for (const auto& entry : mempool_ancestors) {
|
||||
if (ptx->nVersion != 3 && entry->GetTx().nVersion == 3) {
|
||||
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
|
||||
entry->GetSharedTx()->GetHash().ToString(), entry->GetSharedTx()->GetWitnessHash().ToString());
|
||||
} else if (ptx->nVersion == 3 && entry->GetTx().nVersion != 3) {
|
||||
return strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
|
||||
entry->GetSharedTx()->GetHash().ToString(), entry->GetSharedTx()->GetWitnessHash().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// This function is specialized for these limits, and must be reimplemented if they ever change.
|
||||
static_assert(V3_ANCESTOR_LIMIT == 2);
|
||||
static_assert(V3_DESCENDANT_LIMIT == 2);
|
||||
|
||||
// The rest of the rules only apply to transactions with nVersion=3.
|
||||
if (ptx->nVersion != 3) return std::nullopt;
|
||||
|
||||
// Check that V3_ANCESTOR_LIMIT would not be violated, including both in-package and in-mempool.
|
||||
if (mempool_ancestors.size() + 1 > V3_ANCESTOR_LIMIT) {
|
||||
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString());
|
||||
}
|
||||
|
||||
// Remaining checks only pertain to transactions with unconfirmed ancestors.
|
||||
if (mempool_ancestors.size() > 0) {
|
||||
// If this transaction spends V3 parents, it cannot be too large.
|
||||
if (vsize > V3_CHILD_MAX_VSIZE) {
|
||||
return strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
|
||||
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(), vsize, V3_CHILD_MAX_VSIZE);
|
||||
}
|
||||
|
||||
// Check the descendant counts of in-mempool ancestors.
|
||||
const auto& parent_entry = *mempool_ancestors.begin();
|
||||
// If there are any ancestors, this is the only child allowed. The parent cannot have any
|
||||
// other descendants. We handle the possibility of multiple children as that case is
|
||||
// possible through a reorg.
|
||||
const auto& children = parent_entry->GetMemPoolChildrenConst();
|
||||
// Don't double-count a transaction that is going to be replaced. This logic assumes that
|
||||
// any descendant of the V3 transaction is a direct child, which makes sense because a V3
|
||||
// transaction can only have 1 descendant.
|
||||
const bool child_will_be_replaced = !children.empty() &&
|
||||
std::any_of(children.cbegin(), children.cend(),
|
||||
[&direct_conflicts](const CTxMemPoolEntry& child){return direct_conflicts.count(child.GetTx().GetHash()) > 0;});
|
||||
if (parent_entry->GetCountWithDescendants() + 1 > V3_DESCENDANT_LIMIT && !child_will_be_replaced) {
|
||||
return strprintf("tx %u (wtxid=%s) would exceed descendant count limit",
|
||||
parent_entry->GetSharedTx()->GetHash().ToString(),
|
||||
parent_entry->GetSharedTx()->GetWitnessHash().ToString());
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
83
src/policy/v3_policy.h
Normal file
83
src/policy/v3_policy.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 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.
|
||||
|
||||
#ifndef BITCOIN_POLICY_V3_POLICY_H
|
||||
#define BITCOIN_POLICY_V3_POLICY_H
|
||||
|
||||
#include <consensus/amount.h>
|
||||
#include <policy/packages.h>
|
||||
#include <policy/policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <txmempool.h>
|
||||
#include <util/result.h>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
// This module enforces rules for transactions with nVersion=3 ("v3 transactions") which help make
|
||||
// RBF abilities more robust.
|
||||
|
||||
// v3 only allows 1 parent and 1 child when unconfirmed.
|
||||
/** Maximum number of transactions including an unconfirmed tx and its descendants. */
|
||||
static constexpr unsigned int V3_DESCENDANT_LIMIT{2};
|
||||
/** Maximum number of transactions including a V3 tx and all its mempool ancestors. */
|
||||
static constexpr unsigned int V3_ANCESTOR_LIMIT{2};
|
||||
|
||||
/** Maximum sigop-adjusted virtual size of a tx which spends from an unconfirmed v3 transaction. */
|
||||
static constexpr int64_t V3_CHILD_MAX_VSIZE{1000};
|
||||
// These limits are within the default ancestor/descendant limits.
|
||||
static_assert(V3_CHILD_MAX_VSIZE + MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR <= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000);
|
||||
static_assert(V3_CHILD_MAX_VSIZE + MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR <= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * 1000);
|
||||
|
||||
/** Must be called for every transaction, even if not v3. Not strictly necessary for transactions
|
||||
* accepted through AcceptMultipleTransactions.
|
||||
*
|
||||
* Checks the following rules:
|
||||
* 1. A v3 tx must only have v3 unconfirmed ancestors.
|
||||
* 2. A non-v3 tx must only have non-v3 unconfirmed ancestors.
|
||||
* 3. A v3's ancestor set, including itself, must be within V3_ANCESTOR_LIMIT.
|
||||
* 4. A v3's descendant set, including itself, must be within V3_DESCENDANT_LIMIT.
|
||||
* 5. If a v3 tx has any unconfirmed ancestors, the tx's sigop-adjusted vsize must be within
|
||||
* V3_CHILD_MAX_VSIZE.
|
||||
*
|
||||
*
|
||||
* @param[in] mempool_ancestors The in-mempool ancestors of ptx.
|
||||
* @param[in] direct_conflicts In-mempool transactions this tx conflicts with. These conflicts
|
||||
* are used to more accurately calculate the resulting descendant
|
||||
* count of in-mempool ancestors.
|
||||
* @param[in] vsize The sigop-adjusted virtual size of ptx.
|
||||
*
|
||||
* @returns debug string if an error occurs, std::nullopt otherwise.
|
||||
*/
|
||||
std::optional<std::string> SingleV3Checks(const CTransactionRef& ptx,
|
||||
const CTxMemPool::setEntries& mempool_ancestors,
|
||||
const std::set<Txid>& direct_conflicts,
|
||||
int64_t vsize);
|
||||
|
||||
/** Must be called for every transaction that is submitted within a package, even if not v3.
|
||||
*
|
||||
* For each transaction in a package:
|
||||
* If it's not a v3 transaction, verify it has no direct v3 parents in the mempool or the package.
|
||||
|
||||
* If it is a v3 transaction, verify that any direct parents in the mempool or the package are v3.
|
||||
* If such a parent exists, verify that parent has no other children in the package or the mempool,
|
||||
* and that the transaction itself has no children in the package.
|
||||
*
|
||||
* If any v3 violations in the package exist, this test will fail for one of them:
|
||||
* - if a v3 transaction T has a parent in the mempool and a child in the package, then PV3C(T) will fail
|
||||
* - if a v3 transaction T has a parent in the package and a child in the package, then PV3C(T) will fail
|
||||
* - if a v3 transaction T and a v3 (sibling) transaction U have some parent in the mempool,
|
||||
* then PV3C(T) and PV3C(U) will fail
|
||||
* - if a v3 transaction T and a v3 (sibling) transaction U have some parent in the package,
|
||||
* then PV3C(T) and PV3C(U) will fail
|
||||
* - if a v3 transaction T has a parent P and a grandparent G in the package, then
|
||||
* PV3C(P) will fail (though PV3C(G) and PV3C(T) might succeed).
|
||||
*
|
||||
* @returns debug string if an error occurs, std::nullopt otherwise.
|
||||
* */
|
||||
std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t vsize,
|
||||
const Package& package,
|
||||
const CTxMemPool::setEntries& mempool_ancestors);
|
||||
|
||||
#endif // BITCOIN_POLICY_V3_POLICY_H
|
||||
|
|
@ -166,98 +166,98 @@ static RPCHelpMan testmempoolaccept()
|
|||
+ HelpExampleRpc("testmempoolaccept", "[\"signedhex\"]")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
const UniValue raw_transactions = request.params[0].get_array();
|
||||
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER,
|
||||
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
|
||||
}
|
||||
{
|
||||
const UniValue raw_transactions = request.params[0].get_array();
|
||||
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER,
|
||||
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
|
||||
}
|
||||
|
||||
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?
|
||||
DEFAULT_MAX_RAW_TX_FEE_RATE :
|
||||
CFeeRate(AmountFromValue(request.params[1]));
|
||||
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?
|
||||
DEFAULT_MAX_RAW_TX_FEE_RATE :
|
||||
CFeeRate(AmountFromValue(request.params[1]));
|
||||
|
||||
std::vector<CTransactionRef> txns;
|
||||
txns.reserve(raw_transactions.size());
|
||||
for (const auto& rawtx : raw_transactions.getValues()) {
|
||||
CMutableTransaction mtx;
|
||||
if (!DecodeHexTx(mtx, rawtx.get_str())) {
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
|
||||
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
|
||||
}
|
||||
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
|
||||
}
|
||||
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
CTxMemPool& mempool = EnsureMemPool(node);
|
||||
ChainstateManager& chainman = EnsureChainman(node);
|
||||
Chainstate& chainstate = chainman.ActiveChainstate();
|
||||
const PackageMempoolAcceptResult package_result = [&] {
|
||||
LOCK(::cs_main);
|
||||
if (txns.size() > 1) return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/true);
|
||||
return PackageMempoolAcceptResult(txns[0]->GetWitnessHash(),
|
||||
chainman.ProcessTransaction(txns[0], /*test_accept=*/true));
|
||||
}();
|
||||
|
||||
UniValue rpc_result(UniValue::VARR);
|
||||
// We will check transaction fees while we iterate through txns in order. If any transaction fee
|
||||
// exceeds maxfeerate, we will leave the rest of the validation results blank, because it
|
||||
// doesn't make sense to return a validation result for a transaction if its ancestor(s) would
|
||||
// not be submitted.
|
||||
bool exit_early{false};
|
||||
for (const auto& tx : txns) {
|
||||
UniValue result_inner(UniValue::VOBJ);
|
||||
result_inner.pushKV("txid", tx->GetHash().GetHex());
|
||||
result_inner.pushKV("wtxid", tx->GetWitnessHash().GetHex());
|
||||
if (package_result.m_state.GetResult() == PackageValidationResult::PCKG_POLICY) {
|
||||
result_inner.pushKV("package-error", package_result.m_state.GetRejectReason());
|
||||
}
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
if (exit_early || it == package_result.m_tx_results.end()) {
|
||||
// Validation unfinished. Just return the txid and wtxid.
|
||||
rpc_result.push_back(result_inner);
|
||||
continue;
|
||||
}
|
||||
const auto& tx_result = it->second;
|
||||
// Package testmempoolaccept doesn't allow transactions to already be in the mempool.
|
||||
CHECK_NONFATAL(tx_result.m_result_type != MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
|
||||
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
|
||||
const CAmount fee = tx_result.m_base_fees.value();
|
||||
// Check that fee does not exceed maximum fee
|
||||
const int64_t virtual_size = tx_result.m_vsize.value();
|
||||
const CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
|
||||
if (max_raw_tx_fee && fee > max_raw_tx_fee) {
|
||||
result_inner.pushKV("allowed", false);
|
||||
result_inner.pushKV("reject-reason", "max-fee-exceeded");
|
||||
exit_early = true;
|
||||
} else {
|
||||
// Only return the fee and vsize if the transaction would pass ATMP.
|
||||
// These can be used to calculate the feerate.
|
||||
result_inner.pushKV("allowed", true);
|
||||
result_inner.pushKV("vsize", virtual_size);
|
||||
UniValue fees(UniValue::VOBJ);
|
||||
fees.pushKV("base", ValueFromAmount(fee));
|
||||
fees.pushKV("effective-feerate", ValueFromAmount(tx_result.m_effective_feerate.value().GetFeePerK()));
|
||||
UniValue effective_includes_res(UniValue::VARR);
|
||||
for (const auto& wtxid : tx_result.m_wtxids_fee_calculations.value()) {
|
||||
effective_includes_res.push_back(wtxid.ToString());
|
||||
std::vector<CTransactionRef> txns;
|
||||
txns.reserve(raw_transactions.size());
|
||||
for (const auto& rawtx : raw_transactions.getValues()) {
|
||||
CMutableTransaction mtx;
|
||||
if (!DecodeHexTx(mtx, rawtx.get_str())) {
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
|
||||
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
|
||||
}
|
||||
fees.pushKV("effective-includes", effective_includes_res);
|
||||
result_inner.pushKV("fees", fees);
|
||||
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
|
||||
}
|
||||
} else {
|
||||
result_inner.pushKV("allowed", false);
|
||||
const TxValidationState state = tx_result.m_state;
|
||||
if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) {
|
||||
result_inner.pushKV("reject-reason", "missing-inputs");
|
||||
} else {
|
||||
result_inner.pushKV("reject-reason", state.GetRejectReason());
|
||||
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
CTxMemPool& mempool = EnsureMemPool(node);
|
||||
ChainstateManager& chainman = EnsureChainman(node);
|
||||
Chainstate& chainstate = chainman.ActiveChainstate();
|
||||
const PackageMempoolAcceptResult package_result = [&] {
|
||||
LOCK(::cs_main);
|
||||
if (txns.size() > 1) return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/true);
|
||||
return PackageMempoolAcceptResult(txns[0]->GetWitnessHash(),
|
||||
chainman.ProcessTransaction(txns[0], /*test_accept=*/true));
|
||||
}();
|
||||
|
||||
UniValue rpc_result(UniValue::VARR);
|
||||
// We will check transaction fees while we iterate through txns in order. If any transaction fee
|
||||
// exceeds maxfeerate, we will leave the rest of the validation results blank, because it
|
||||
// doesn't make sense to return a validation result for a transaction if its ancestor(s) would
|
||||
// not be submitted.
|
||||
bool exit_early{false};
|
||||
for (const auto& tx : txns) {
|
||||
UniValue result_inner(UniValue::VOBJ);
|
||||
result_inner.pushKV("txid", tx->GetHash().GetHex());
|
||||
result_inner.pushKV("wtxid", tx->GetWitnessHash().GetHex());
|
||||
if (package_result.m_state.GetResult() == PackageValidationResult::PCKG_POLICY) {
|
||||
result_inner.pushKV("package-error", package_result.m_state.ToString());
|
||||
}
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
if (exit_early || it == package_result.m_tx_results.end()) {
|
||||
// Validation unfinished. Just return the txid and wtxid.
|
||||
rpc_result.push_back(result_inner);
|
||||
continue;
|
||||
}
|
||||
const auto& tx_result = it->second;
|
||||
// Package testmempoolaccept doesn't allow transactions to already be in the mempool.
|
||||
CHECK_NONFATAL(tx_result.m_result_type != MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
|
||||
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
|
||||
const CAmount fee = tx_result.m_base_fees.value();
|
||||
// Check that fee does not exceed maximum fee
|
||||
const int64_t virtual_size = tx_result.m_vsize.value();
|
||||
const CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
|
||||
if (max_raw_tx_fee && fee > max_raw_tx_fee) {
|
||||
result_inner.pushKV("allowed", false);
|
||||
result_inner.pushKV("reject-reason", "max-fee-exceeded");
|
||||
exit_early = true;
|
||||
} else {
|
||||
// Only return the fee and vsize if the transaction would pass ATMP.
|
||||
// These can be used to calculate the feerate.
|
||||
result_inner.pushKV("allowed", true);
|
||||
result_inner.pushKV("vsize", virtual_size);
|
||||
UniValue fees(UniValue::VOBJ);
|
||||
fees.pushKV("base", ValueFromAmount(fee));
|
||||
fees.pushKV("effective-feerate", ValueFromAmount(tx_result.m_effective_feerate.value().GetFeePerK()));
|
||||
UniValue effective_includes_res(UniValue::VARR);
|
||||
for (const auto& wtxid : tx_result.m_wtxids_fee_calculations.value()) {
|
||||
effective_includes_res.push_back(wtxid.ToString());
|
||||
}
|
||||
fees.pushKV("effective-includes", effective_includes_res);
|
||||
result_inner.pushKV("fees", fees);
|
||||
}
|
||||
} else {
|
||||
result_inner.pushKV("allowed", false);
|
||||
const TxValidationState state = tx_result.m_state;
|
||||
if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) {
|
||||
result_inner.pushKV("reject-reason", "missing-inputs");
|
||||
} else {
|
||||
result_inner.pushKV("reject-reason", state.GetRejectReason());
|
||||
}
|
||||
}
|
||||
rpc_result.push_back(result_inner);
|
||||
}
|
||||
}
|
||||
rpc_result.push_back(result_inner);
|
||||
}
|
||||
return rpc_result;
|
||||
},
|
||||
return rpc_result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -918,7 +918,7 @@ static RPCHelpMan submitpackage()
|
|||
case PackageValidationResult::PCKG_TX:
|
||||
{
|
||||
// Package-wide error we want to return, but we also want to return individual responses
|
||||
package_msg = package_result.m_state.GetRejectReason();
|
||||
package_msg = package_result.m_state.ToString();
|
||||
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size() ||
|
||||
package_result.m_tx_results.empty());
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <node/context.h>
|
||||
#include <node/mempool_args.h>
|
||||
#include <node/miner.h>
|
||||
#include <policy/v3_policy.h>
|
||||
#include <test/fuzz/FuzzedDataProvider.h>
|
||||
#include <test/fuzz/fuzz.h>
|
||||
#include <test/fuzz/util.h>
|
||||
|
|
@ -119,7 +120,8 @@ CTxMemPool MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeConte
|
|||
mempool_opts.limits.descendant_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202) * 1'000;
|
||||
mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200) * 1'000'000;
|
||||
mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)};
|
||||
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(1, 999);
|
||||
// Only interested in 2 cases: sigop cost 0 or when single legacy sigop cost is >> 1KvB
|
||||
nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 1) * 10'000;
|
||||
|
||||
mempool_opts.check_ratio = 1;
|
||||
mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
|
||||
|
|
@ -171,11 +173,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
// Create transaction to add to the mempool
|
||||
const CTransactionRef tx = [&] {
|
||||
CMutableTransaction tx_mut;
|
||||
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
|
||||
// Last tx will sweep all outpoints in package
|
||||
const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size());
|
||||
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
|
||||
auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
|
||||
|
||||
auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
|
||||
|
||||
|
|
@ -215,17 +217,24 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
tx_mut.vin.push_back(tx_mut.vin.back());
|
||||
}
|
||||
|
||||
// Refer to a non-existant input
|
||||
// Refer to a non-existent input
|
||||
if (fuzzed_data_provider.ConsumeBool()) {
|
||||
tx_mut.vin.emplace_back();
|
||||
}
|
||||
|
||||
// Make a p2pk output to make sigops adjusted vsize to violate v3, potentially, which is never spent
|
||||
if (last_tx && amount_in > 1000 && fuzzed_data_provider.ConsumeBool()) {
|
||||
tx_mut.vout.emplace_back(CAsset(), 1000, CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG);
|
||||
// Don't add any other outputs.
|
||||
num_out = 1;
|
||||
amount_in -= 1000;
|
||||
}
|
||||
|
||||
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(CAsset(), amount_out, P2WSH_EMPTY);
|
||||
}
|
||||
// TODO vary transaction sizes to catch size-related issues
|
||||
auto tx = MakeTransactionRef(tx_mut);
|
||||
// Restore previously removed outpoints, except in-package outpoints
|
||||
if (!last_tx) {
|
||||
|
|
@ -265,7 +274,6 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
std::set<CTransactionRef> added;
|
||||
auto txr = std::make_shared<TransactionsDelta>(added);
|
||||
RegisterSharedValidationInterface(txr);
|
||||
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
|
||||
|
||||
// When there are multiple transactions in the package, we call ProcessNewPackage(txs, test_accept=false)
|
||||
// and AcceptToMemoryPool(txs.back(), test_accept=true). When there is only 1 transaction, we might flip it
|
||||
|
|
@ -275,17 +283,20 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
const auto result_package = WITH_LOCK(::cs_main,
|
||||
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit));
|
||||
|
||||
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), bypass_limits, /*test_accept=*/!single_submit));
|
||||
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
|
||||
// Always set bypass_limits to false because it is not supported in ProcessNewPackage and
|
||||
// can be a source of divergence.
|
||||
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
|
||||
/*bypass_limits=*/false, /*test_accept=*/!single_submit));
|
||||
const bool passed = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
|
||||
|
||||
SyncWithValidationInterfaceQueue();
|
||||
UnregisterSharedValidationInterface(txr);
|
||||
|
||||
// There is only 1 transaction in the package. We did a test-package-accept and a ATMP
|
||||
if (single_submit) {
|
||||
Assert(accepted != added.empty());
|
||||
Assert(accepted == res.m_state.IsValid());
|
||||
if (accepted) {
|
||||
Assert(passed != added.empty());
|
||||
Assert(passed == res.m_state.IsValid());
|
||||
if (passed) {
|
||||
Assert(added.size() == 1);
|
||||
Assert(txs.back() == *added.begin());
|
||||
}
|
||||
|
|
@ -299,6 +310,8 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
|
|||
// This is empty if it fails early checks, or "full" if transactions are looked at deeper
|
||||
Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty());
|
||||
}
|
||||
|
||||
CheckMempoolV3Invariants(tx_pool);
|
||||
}
|
||||
|
||||
UnregisterSharedValidationInterface(outpoints_updater);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <node/context.h>
|
||||
#include <node/mempool_args.h>
|
||||
#include <node/miner.h>
|
||||
#include <policy/v3_policy.h>
|
||||
#include <test/fuzz/FuzzedDataProvider.h>
|
||||
#include <test/fuzz/fuzz.h>
|
||||
#include <test/fuzz/util.h>
|
||||
|
|
@ -227,7 +228,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
|
|||
// Create transaction to add to the mempool
|
||||
const CTransactionRef tx = [&] {
|
||||
CMutableTransaction tx_mut;
|
||||
tx_mut.nVersion = CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION;
|
||||
tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
|
||||
const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size());
|
||||
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size() * 2);
|
||||
|
|
@ -316,6 +317,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
|
|||
if (accepted) {
|
||||
Assert(added.size() == 1); // For now, no package acceptance
|
||||
Assert(tx == *added.begin());
|
||||
CheckMempoolV3Invariants(tx_pool);
|
||||
} else {
|
||||
// Do not consider rejected transaction removed
|
||||
removed.erase(tx);
|
||||
|
|
@ -408,6 +410,9 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
|
|||
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
|
||||
if (accepted) {
|
||||
txids.push_back(tx->GetHash());
|
||||
// Only check fees if accepted and not bypass_limits, otherwise it's not guaranteed that
|
||||
// trimming has happened for this tx and previous iterations.
|
||||
CheckMempoolV3Invariants(tx_pool);
|
||||
}
|
||||
}
|
||||
Finish(fuzzed_data_provider, tx_pool, chainstate);
|
||||
|
|
|
|||
|
|
@ -136,8 +136,6 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup)
|
|||
// Tests for EntriesAndTxidsDisjoint
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint(empty_set, {tx1->GetHash()}, unused_txid) == std::nullopt);
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx3->GetHash()}, unused_txid) == std::nullopt);
|
||||
// EntriesAndTxidsDisjoint uses txids, not wtxids.
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2}, {tx2->GetWitnessHash()}, unused_txid) == std::nullopt);
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2}, {tx2->GetHash()}, unused_txid).has_value());
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx1->GetHash()}, unused_txid).has_value());
|
||||
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx2->GetHash()}, unused_txid).has_value());
|
||||
|
|
|
|||
|
|
@ -803,7 +803,7 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
|
|||
t.nVersion = 0;
|
||||
CheckIsNotStandard(t, "version");
|
||||
|
||||
t.nVersion = 3;
|
||||
t.nVersion = TX_MAX_STANDARD_VERSION + 1;
|
||||
CheckIsNotStandard(t, "version");
|
||||
|
||||
// Allowed nVersion
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
|
||||
#include <consensus/validation.h>
|
||||
#include <key_io.h>
|
||||
#include <policy/v3_policy.h>
|
||||
#include <policy/packages.h>
|
||||
#include <policy/policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
#include <script/script.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <test/util/txmempool.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
|
@ -48,4 +51,291 @@ BOOST_FIXTURE_TEST_CASE(tx_mempool_reject_coinbase, TestChain100Setup)
|
|||
BOOST_CHECK_EQUAL(result.m_state.GetRejectReason(), "coinbase");
|
||||
BOOST_CHECK(result.m_state.GetResult() == TxValidationResult::TX_CONSENSUS);
|
||||
}
|
||||
|
||||
// Generate a number of random, nonexistent outpoints.
|
||||
static inline std::vector<COutPoint> random_outpoints(size_t num_outpoints) {
|
||||
std::vector<COutPoint> outpoints;
|
||||
for (size_t i{0}; i < num_outpoints; ++i) {
|
||||
outpoints.emplace_back(Txid::FromUint256(GetRandHash()), 0);
|
||||
}
|
||||
return outpoints;
|
||||
}
|
||||
|
||||
static inline std::vector<CPubKey> random_keys(size_t num_keys) {
|
||||
std::vector<CPubKey> keys;
|
||||
keys.reserve(num_keys);
|
||||
for (size_t i{0}; i < num_keys; ++i) {
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
keys.emplace_back(key.GetPubKey());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Creates a placeholder tx (not valid) with 25 outputs. Specify the nVersion and the inputs.
|
||||
static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, int32_t version)
|
||||
{
|
||||
CMutableTransaction mtx = CMutableTransaction{};
|
||||
mtx.nVersion = version;
|
||||
mtx.vin.resize(inputs.size());
|
||||
mtx.vout.resize(25);
|
||||
for (size_t i{0}; i < inputs.size(); ++i) {
|
||||
mtx.vin[i].prevout = inputs[i];
|
||||
}
|
||||
for (auto i{0}; i < 25; ++i) {
|
||||
mtx.vout[i].scriptPubKey = CScript() << OP_TRUE;
|
||||
mtx.vout[i].nValue = 10000;
|
||||
}
|
||||
return MakeTransactionRef(mtx);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(version3_tests, RegTestingSetup)
|
||||
{
|
||||
// Test V3 policy helper functions
|
||||
CTxMemPool& pool = *Assert(m_node.mempool);
|
||||
LOCK2(cs_main, pool.cs);
|
||||
TestMemPoolEntryHelper entry;
|
||||
std::set<Txid> empty_conflicts_set;
|
||||
CTxMemPool::setEntries empty_ancestors;
|
||||
|
||||
auto mempool_tx_v3 = make_tx(random_outpoints(1), /*version=*/3);
|
||||
pool.addUnchecked(entry.FromTx(mempool_tx_v3));
|
||||
auto mempool_tx_v2 = make_tx(random_outpoints(1), /*version=*/2);
|
||||
pool.addUnchecked(entry.FromTx(mempool_tx_v2));
|
||||
// Default values.
|
||||
CTxMemPool::Limits m_limits{};
|
||||
|
||||
// Cannot spend from an unconfirmed v3 transaction unless this tx is also v3.
|
||||
{
|
||||
// mempool_tx_v3
|
||||
// ^
|
||||
// tx_v2_from_v3
|
||||
auto tx_v2_from_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/2);
|
||||
auto ancestors_v2_from_v3{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v3), m_limits)};
|
||||
const auto expected_error_str{strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
|
||||
tx_v2_from_v3->GetHash().ToString(), tx_v2_from_v3->GetWitnessHash().ToString(),
|
||||
mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK(*SingleV3Checks(tx_v2_from_v3, *ancestors_v2_from_v3, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v3)) == expected_error_str);
|
||||
|
||||
Package package_v3_v2{mempool_tx_v3, tx_v2_from_v3};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v2_from_v3, GetVirtualTransactionSize(*tx_v2_from_v3), package_v3_v2, empty_ancestors), expected_error_str);
|
||||
|
||||
// mempool_tx_v3 mempool_tx_v2
|
||||
// ^ ^
|
||||
// tx_v2_from_v2_and_v3
|
||||
auto tx_v2_from_v2_and_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}, COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/2);
|
||||
auto ancestors_v2_from_both{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v2_and_v3), m_limits)};
|
||||
const auto expected_error_str_2{strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
|
||||
tx_v2_from_v2_and_v3->GetHash().ToString(), tx_v2_from_v2_and_v3->GetWitnessHash().ToString(),
|
||||
mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK(*SingleV3Checks(tx_v2_from_v2_and_v3, *ancestors_v2_from_both, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v2_and_v3))
|
||||
== expected_error_str_2);
|
||||
|
||||
Package package_v3_v2_v2{mempool_tx_v3, mempool_tx_v2, tx_v2_from_v2_and_v3};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v2_from_v2_and_v3, GetVirtualTransactionSize(*tx_v2_from_v2_and_v3), package_v3_v2_v2, empty_ancestors), expected_error_str_2);
|
||||
}
|
||||
|
||||
// V3 cannot spend from an unconfirmed non-v3 transaction.
|
||||
{
|
||||
// mempool_tx_v2
|
||||
// ^
|
||||
// tx_v3_from_v2
|
||||
auto tx_v3_from_v2 = make_tx({COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/3);
|
||||
auto ancestors_v3_from_v2{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v2), m_limits)};
|
||||
const auto expected_error_str{strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
|
||||
tx_v3_from_v2->GetHash().ToString(), tx_v3_from_v2->GetWitnessHash().ToString(),
|
||||
mempool_tx_v2->GetHash().ToString(), mempool_tx_v2->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK(*SingleV3Checks(tx_v3_from_v2, *ancestors_v3_from_v2, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v2)) == expected_error_str);
|
||||
|
||||
Package package_v2_v3{mempool_tx_v2, tx_v3_from_v2};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_from_v2, GetVirtualTransactionSize(*tx_v3_from_v2), package_v2_v3, empty_ancestors), expected_error_str);
|
||||
|
||||
// mempool_tx_v3 mempool_tx_v2
|
||||
// ^ ^
|
||||
// tx_v3_from_v2_and_v3
|
||||
auto tx_v3_from_v2_and_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}, COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/3);
|
||||
auto ancestors_v3_from_both{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v2_and_v3), m_limits)};
|
||||
const auto expected_error_str_2{strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
|
||||
tx_v3_from_v2_and_v3->GetHash().ToString(), tx_v3_from_v2_and_v3->GetWitnessHash().ToString(),
|
||||
mempool_tx_v2->GetHash().ToString(), mempool_tx_v2->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK(*SingleV3Checks(tx_v3_from_v2_and_v3, *ancestors_v3_from_both, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v2_and_v3))
|
||||
== expected_error_str_2);
|
||||
|
||||
// tx_v3_from_v2_and_v3 also violates V3_ANCESTOR_LIMIT.
|
||||
const auto expected_error_str_3{strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
tx_v3_from_v2_and_v3->GetHash().ToString(), tx_v3_from_v2_and_v3->GetWitnessHash().ToString())};
|
||||
Package package_v3_v2_v3{mempool_tx_v3, mempool_tx_v2, tx_v3_from_v2_and_v3};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_from_v2_and_v3, GetVirtualTransactionSize(*tx_v3_from_v2_and_v3), package_v3_v2_v3, empty_ancestors), expected_error_str_3);
|
||||
}
|
||||
// V3 from V3 is ok, and non-V3 from non-V3 is ok.
|
||||
{
|
||||
// mempool_tx_v3
|
||||
// ^
|
||||
// tx_v3_from_v3
|
||||
auto tx_v3_from_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/3);
|
||||
auto ancestors_v3{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v3), m_limits)};
|
||||
BOOST_CHECK(SingleV3Checks(tx_v3_from_v3, *ancestors_v3, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v3))
|
||||
== std::nullopt);
|
||||
|
||||
Package package_v3_v3{mempool_tx_v3, tx_v3_from_v3};
|
||||
BOOST_CHECK(PackageV3Checks(tx_v3_from_v3, GetVirtualTransactionSize(*tx_v3_from_v3), package_v3_v3, empty_ancestors) == std::nullopt);
|
||||
|
||||
// mempool_tx_v2
|
||||
// ^
|
||||
// tx_v2_from_v2
|
||||
auto tx_v2_from_v2 = make_tx({COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/2);
|
||||
auto ancestors_v2{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v2), m_limits)};
|
||||
BOOST_CHECK(SingleV3Checks(tx_v2_from_v2, *ancestors_v2, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v2))
|
||||
== std::nullopt);
|
||||
|
||||
Package package_v2_v2{mempool_tx_v2, tx_v2_from_v2};
|
||||
BOOST_CHECK(PackageV3Checks(tx_v2_from_v2, GetVirtualTransactionSize(*tx_v2_from_v2), package_v2_v2, empty_ancestors) == std::nullopt);
|
||||
}
|
||||
|
||||
// Tx spending v3 cannot have too many mempool ancestors
|
||||
// Configuration where the tx has multiple direct parents.
|
||||
{
|
||||
Package package_multi_parents;
|
||||
std::vector<COutPoint> mempool_outpoints;
|
||||
mempool_outpoints.emplace_back(mempool_tx_v3->GetHash(), 0);
|
||||
package_multi_parents.emplace_back(mempool_tx_v3);
|
||||
for (size_t i{0}; i < 2; ++i) {
|
||||
auto mempool_tx = make_tx(random_outpoints(i + 1), /*version=*/3);
|
||||
pool.addUnchecked(entry.FromTx(mempool_tx));
|
||||
mempool_outpoints.emplace_back(mempool_tx->GetHash(), 0);
|
||||
package_multi_parents.emplace_back(mempool_tx);
|
||||
}
|
||||
auto tx_v3_multi_parent = make_tx(mempool_outpoints, /*version=*/3);
|
||||
package_multi_parents.emplace_back(tx_v3_multi_parent);
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_multi_parent), m_limits)};
|
||||
BOOST_CHECK_EQUAL(ancestors->size(), 3);
|
||||
const auto expected_error_str{strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
tx_v3_multi_parent->GetHash().ToString(), tx_v3_multi_parent->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK_EQUAL(*SingleV3Checks(tx_v3_multi_parent, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_multi_parent)),
|
||||
expected_error_str);
|
||||
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_multi_parent, GetVirtualTransactionSize(*tx_v3_multi_parent), package_multi_parents, empty_ancestors),
|
||||
expected_error_str);
|
||||
}
|
||||
|
||||
// Configuration where the tx is in a multi-generation chain.
|
||||
{
|
||||
Package package_multi_gen;
|
||||
CTransactionRef middle_tx;
|
||||
auto last_outpoint{random_outpoints(1)[0]};
|
||||
for (size_t i{0}; i < 2; ++i) {
|
||||
auto mempool_tx = make_tx({last_outpoint}, /*version=*/3);
|
||||
pool.addUnchecked(entry.FromTx(mempool_tx));
|
||||
last_outpoint = COutPoint{mempool_tx->GetHash(), 0};
|
||||
package_multi_gen.emplace_back(mempool_tx);
|
||||
if (i == 1) middle_tx = mempool_tx;
|
||||
}
|
||||
auto tx_v3_multi_gen = make_tx({last_outpoint}, /*version=*/3);
|
||||
package_multi_gen.emplace_back(tx_v3_multi_gen);
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_multi_gen), m_limits)};
|
||||
const auto expected_error_str{strprintf("tx %s (wtxid=%s) would have too many ancestors",
|
||||
tx_v3_multi_gen->GetHash().ToString(), tx_v3_multi_gen->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK_EQUAL(*SingleV3Checks(tx_v3_multi_gen, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_multi_gen)),
|
||||
expected_error_str);
|
||||
|
||||
// Middle tx is what triggers a failure for the grandchild:
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(middle_tx, GetVirtualTransactionSize(*middle_tx), package_multi_gen, empty_ancestors), expected_error_str);
|
||||
BOOST_CHECK(PackageV3Checks(tx_v3_multi_gen, GetVirtualTransactionSize(*tx_v3_multi_gen), package_multi_gen, empty_ancestors) == std::nullopt);
|
||||
}
|
||||
|
||||
// Tx spending v3 cannot be too large in virtual size.
|
||||
auto many_inputs{random_outpoints(100)};
|
||||
many_inputs.emplace_back(mempool_tx_v3->GetHash(), 0);
|
||||
{
|
||||
auto tx_v3_child_big = make_tx(many_inputs, /*version=*/3);
|
||||
const auto vsize{GetVirtualTransactionSize(*tx_v3_child_big)};
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_child_big), m_limits)};
|
||||
const auto expected_error_str{strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
|
||||
tx_v3_child_big->GetHash().ToString(), tx_v3_child_big->GetWitnessHash().ToString(), vsize, V3_CHILD_MAX_VSIZE)};
|
||||
BOOST_CHECK_EQUAL(*SingleV3Checks(tx_v3_child_big, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_child_big)),
|
||||
expected_error_str);
|
||||
|
||||
Package package_child_big{mempool_tx_v3, tx_v3_child_big};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_child_big, GetVirtualTransactionSize(*tx_v3_child_big), package_child_big, empty_ancestors),
|
||||
expected_error_str);
|
||||
}
|
||||
|
||||
// Tx spending v3 cannot have too many sigops.
|
||||
// This child has 10 P2WSH multisig inputs.
|
||||
auto multisig_outpoints{random_outpoints(10)};
|
||||
multisig_outpoints.emplace_back(mempool_tx_v3->GetHash(), 0);
|
||||
auto keys{random_keys(2)};
|
||||
CScript script_multisig;
|
||||
script_multisig << OP_1;
|
||||
for (const auto& key : keys) {
|
||||
script_multisig << ToByteVector(key);
|
||||
}
|
||||
script_multisig << OP_2 << OP_CHECKMULTISIG;
|
||||
{
|
||||
CMutableTransaction mtx_many_sigops = CMutableTransaction{};
|
||||
mtx_many_sigops.nVersion = 3;
|
||||
mtx_many_sigops.witness.vtxinwit.resize(multisig_outpoints.size()); // ELEMENTS
|
||||
for (const auto& outpoint : multisig_outpoints) {
|
||||
mtx_many_sigops.vin.emplace_back(outpoint);
|
||||
mtx_many_sigops.witness.vtxinwit.back().scriptWitness.stack.emplace_back(script_multisig.begin(), script_multisig.end()); // ELEMENTS
|
||||
}
|
||||
mtx_many_sigops.vout.resize(1);
|
||||
mtx_many_sigops.vout.back().scriptPubKey = CScript() << OP_TRUE;
|
||||
mtx_many_sigops.vout.back().nValue = 10000;
|
||||
auto tx_many_sigops{MakeTransactionRef(mtx_many_sigops)};
|
||||
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_many_sigops), m_limits)};
|
||||
// legacy uses fAccurate = false, and the maximum number of multisig keys is used
|
||||
const int64_t total_sigops{static_cast<int64_t>(tx_many_sigops->vin.size()) * static_cast<int64_t>(script_multisig.GetSigOpCount(/*fAccurate=*/false))};
|
||||
BOOST_CHECK_EQUAL(total_sigops, tx_many_sigops->vin.size() * MAX_PUBKEYS_PER_MULTISIG);
|
||||
const int64_t bip141_vsize{GetVirtualTransactionSize(*tx_many_sigops)};
|
||||
// Weight limit is not reached...
|
||||
BOOST_CHECK(SingleV3Checks(tx_many_sigops, *ancestors, empty_conflicts_set, bip141_vsize) == std::nullopt);
|
||||
// ...but sigop limit is.
|
||||
const auto expected_error_str{strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
|
||||
tx_many_sigops->GetHash().ToString(), tx_many_sigops->GetWitnessHash().ToString(),
|
||||
total_sigops * DEFAULT_BYTES_PER_SIGOP / WITNESS_SCALE_FACTOR, V3_CHILD_MAX_VSIZE)};
|
||||
BOOST_CHECK_EQUAL(*SingleV3Checks(tx_many_sigops, *ancestors, empty_conflicts_set,
|
||||
GetVirtualTransactionSize(*tx_many_sigops, /*nSigOpCost=*/total_sigops, /*bytes_per_sigop=*/ DEFAULT_BYTES_PER_SIGOP)),
|
||||
expected_error_str);
|
||||
|
||||
Package package_child_sigops{mempool_tx_v3, tx_many_sigops};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_many_sigops, total_sigops * DEFAULT_BYTES_PER_SIGOP / WITNESS_SCALE_FACTOR, package_child_sigops, empty_ancestors),
|
||||
expected_error_str);
|
||||
}
|
||||
|
||||
// Parent + child with v3 in the mempool. Child is allowed as long as it is under V3_CHILD_MAX_VSIZE.
|
||||
auto tx_mempool_v3_child = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/3);
|
||||
{
|
||||
BOOST_CHECK(GetTransactionWeight(*tx_mempool_v3_child) <= V3_CHILD_MAX_VSIZE * WITNESS_SCALE_FACTOR);
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_mempool_v3_child), m_limits)};
|
||||
BOOST_CHECK(SingleV3Checks(tx_mempool_v3_child, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_mempool_v3_child)) == std::nullopt);
|
||||
pool.addUnchecked(entry.FromTx(tx_mempool_v3_child));
|
||||
|
||||
Package package_v3_1p1c{mempool_tx_v3, tx_mempool_v3_child};
|
||||
BOOST_CHECK(PackageV3Checks(tx_mempool_v3_child, GetVirtualTransactionSize(*tx_mempool_v3_child), package_v3_1p1c, empty_ancestors) == std::nullopt);
|
||||
}
|
||||
|
||||
// A v3 transaction cannot have more than 1 descendant.
|
||||
// Configuration where tx has multiple direct children.
|
||||
{
|
||||
auto tx_v3_child2 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 1}}, /*version=*/3);
|
||||
auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_child2), m_limits)};
|
||||
const auto expected_error_str{strprintf("tx %s (wtxid=%s) would exceed descendant count limit",
|
||||
mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())};
|
||||
BOOST_CHECK_EQUAL(*SingleV3Checks(tx_v3_child2, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_child2)),
|
||||
expected_error_str);
|
||||
// If replacing the child, make sure there is no double-counting.
|
||||
BOOST_CHECK(SingleV3Checks(tx_v3_child2, *ancestors, {tx_mempool_v3_child->GetHash()}, GetVirtualTransactionSize(*tx_v3_child2))
|
||||
== std::nullopt);
|
||||
|
||||
Package package_v3_1p2c{mempool_tx_v3, tx_mempool_v3_child, tx_v3_child2};
|
||||
BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_child2, GetVirtualTransactionSize(*tx_v3_child2), package_v3_1p2c, empty_ancestors),
|
||||
expected_error_str);
|
||||
}
|
||||
|
||||
// Configuration where tx has multiple generations of descendants is not tested because that is
|
||||
// equivalent to the tx with multiple generations of ancestors.
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <chainparams.h>
|
||||
#include <node/context.h>
|
||||
#include <node/mempool_args.h>
|
||||
#include <policy/v3_policy.h>
|
||||
#include <txmempool.h>
|
||||
#include <util/check.h>
|
||||
#include <util/time.h>
|
||||
|
|
@ -117,3 +118,28 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
|
|||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CheckMempoolV3Invariants(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()));
|
||||
if (tx_info.tx->nVersion == 3) {
|
||||
// Check that special v3 ancestor/descendant limits and rules are always respected
|
||||
Assert(entry.GetCountWithDescendants() <= V3_DESCENDANT_LIMIT);
|
||||
Assert(entry.GetCountWithAncestors() <= V3_ANCESTOR_LIMIT);
|
||||
// If this transaction has at least 1 ancestor, it's a "child" and has restricted weight.
|
||||
if (entry.GetCountWithAncestors() > 1) {
|
||||
Assert(entry.GetTxSize() <= V3_CHILD_MAX_VSIZE);
|
||||
// All v3 transactions must only have v3 unconfirmed parents.
|
||||
const auto& parents = entry.GetMemPoolParentsConst();
|
||||
Assert(parents.begin()->get().GetSharedTx()->nVersion == 3);
|
||||
}
|
||||
} else if (entry.GetCountWithAncestors() > 1) {
|
||||
// All non-v3 transactions must only have non-v3 unconfirmed parents.
|
||||
for (const auto& parent : entry.GetMemPoolParentsConst()) {
|
||||
Assert(parent.get().GetSharedTx()->nVersion != 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,4 +50,14 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
|
|||
const PackageMempoolAcceptResult& result,
|
||||
bool expect_valid,
|
||||
const CTxMemPool* mempool);
|
||||
|
||||
/** For every transaction in tx_pool, check v3 invariants:
|
||||
* - a v3 tx's ancestor count must be within V3_ANCESTOR_LIMIT
|
||||
* - a v3 tx's descendant count must be within V3_DESCENDANT_LIMIT
|
||||
* - if a v3 tx has ancestors, its sigop-adjusted vsize must be within V3_CHILD_MAX_VSIZE
|
||||
* - any non-v3 tx must only have non-v3 parents
|
||||
* - any v3 tx must only have v3 parents
|
||||
* */
|
||||
void CheckMempoolV3Invariants(const CTxMemPool& tx_pool);
|
||||
|
||||
#endif // BITCOIN_TEST_UTIL_TXMEMPOOL_H
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ void CTxMemPool::addUnchecked(const CTxMemPoolEntry &entry, setEntries &setAnces
|
|||
cachedInnerUsage += entry.DynamicMemoryUsage();
|
||||
|
||||
const CTransaction& tx = newit->GetTx();
|
||||
std::set<uint256> setParentTransactions;
|
||||
std::set<Txid> setParentTransactions;
|
||||
for (unsigned int i = 0; i < tx.vin.size(); i++) {
|
||||
mapNextTx.insert(std::make_pair(&tx.vin[i].prevout, &tx));
|
||||
setParentTransactions.insert(tx.vin[i].prevout.hash);
|
||||
|
|
@ -1031,7 +1031,7 @@ std::optional<CTxMemPool::txiter> CTxMemPool::GetIter(const uint256& txid) const
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
CTxMemPool::setEntries CTxMemPool::GetIterSet(const std::set<uint256>& hashes) const
|
||||
CTxMemPool::setEntries CTxMemPool::GetIterSet(const std::set<Txid>& hashes) const
|
||||
{
|
||||
CTxMemPool::setEntries ret;
|
||||
for (const auto& h : hashes) {
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ public:
|
|||
/** Translate a set of hashes into a set of pool iterators to avoid repeated lookups.
|
||||
* Does not require that all of the hashes correspond to actual transactions in the mempool,
|
||||
* only returns the ones that exist. */
|
||||
setEntries GetIterSet(const std::set<uint256>& hashes) const EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
setEntries GetIterSet(const std::set<Txid>& hashes) const EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
|
||||
/** Translate a list of hashes into a list of mempool iterators to avoid repeated lookups.
|
||||
* The nth element in txids becomes the nth element in the returned vector. If any of the txids
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
#include <node/blockstorage.h>
|
||||
#include <node/utxo_snapshot.h>
|
||||
#include <pegins.h>
|
||||
#include <policy/v3_policy.h>
|
||||
#include <policy/policy.h>
|
||||
#include <policy/rbf.h>
|
||||
#include <policy/settings.h>
|
||||
|
|
@ -346,7 +347,9 @@ void Chainstate::MaybeUpdateMempoolForReorg(
|
|||
// Also updates valid entries' cached LockPoints if needed.
|
||||
// If false, the tx is still valid and its lockpoints are updated.
|
||||
// If true, the tx would be invalid in the next block; remove this entry and all of its descendants.
|
||||
const auto filter_final_and_mature = [this](CTxMemPool::txiter it)
|
||||
// Note that v3 rules are not applied here, so reorgs may cause violations of v3 inheritance or
|
||||
// topology restrictions.
|
||||
const auto filter_final_and_mature = [&](CTxMemPool::txiter it)
|
||||
EXCLUSIVE_LOCKS_REQUIRED(m_mempool->cs, ::cs_main) {
|
||||
AssertLockHeld(m_mempool->cs);
|
||||
AssertLockHeld(::cs_main);
|
||||
|
|
@ -601,8 +604,9 @@ private:
|
|||
// explicit Workspace(const CTransactionRef& ptx) : m_ptx(ptx), m_hash(ptx->GetHash()) {} // ELEMENTS: unused since we need the genesis block hash
|
||||
explicit Workspace(const CTransactionRef& ptx, const uint256& hash_genesis_block) : m_ptx(ptx), m_hash(ptx->GetHash()), m_precomputed_txdata(hash_genesis_block) {} // ELEMENTS
|
||||
/** Txids of mempool transactions that this transaction directly conflicts with. */
|
||||
std::set<uint256> m_conflicts;
|
||||
std::set<std::pair<uint256, COutPoint> > m_set_pegins_spent;
|
||||
std::set<Txid> m_conflicts;
|
||||
/** ELEMENTS: keep a set of spent peg-ins */
|
||||
std::set<std::pair<uint256, COutPoint>> m_set_pegins_spent;
|
||||
/** Iterators to mempool entries that this transaction directly conflicts with. */
|
||||
CTxMemPool::setEntries m_iters_conflicting;
|
||||
/** Iterators to all mempool entries that would be replaced by this transaction, including
|
||||
|
|
@ -794,9 +798,12 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
|||
// check all unconfirmed ancestors; otherwise an opt-in ancestor
|
||||
// might be replaced, causing removal of this descendant.
|
||||
//
|
||||
// If replaceability signaling is ignored due to node setting,
|
||||
// replacement is always allowed.
|
||||
if (!m_pool.m_full_rbf && !SignalsOptInRBF(*ptxConflicting)) {
|
||||
// All V3 transactions are considered replaceable.
|
||||
//
|
||||
// Replaceability signaling of the original transactions may be
|
||||
// ignored due to node setting.
|
||||
const bool allow_rbf{m_pool.m_full_rbf || SignalsOptInRBF(*ptxConflicting) || ptxConflicting->nVersion == 3};
|
||||
if (!allow_rbf) {
|
||||
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-conflict");
|
||||
}
|
||||
|
||||
|
|
@ -958,7 +965,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
|||
// while a tx could be package CPFP'd when entering the mempool, we do not have a DoS-resistant
|
||||
// method of ensuring the tx remains bumped. For example, the fee-bumping child could disappear
|
||||
// due to a replacement.
|
||||
if (!bypass_limits && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(package_size)) {
|
||||
// The only exception is v3 transactions.
|
||||
if (!bypass_limits && ws.m_ptx->nVersion != 3 && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(package_size)) {
|
||||
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
|
||||
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
|
||||
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
|
||||
|
|
@ -1040,6 +1048,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
|||
}
|
||||
|
||||
ws.m_ancestors = *ancestors;
|
||||
if (const auto err_string{SingleV3Checks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
|
||||
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "v3-rule-violation", *err_string);
|
||||
}
|
||||
|
||||
// A transaction that spends outputs that would be replaced by it is invalid. Now
|
||||
// that we have the set of all ancestors we can detect this
|
||||
|
|
@ -1408,6 +1419,15 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
|
|||
m_viewmempool.PackageAddTransaction(ws.m_ptx);
|
||||
}
|
||||
|
||||
// At this point we have all in-mempool ancestors, and we know every transaction's vsize.
|
||||
// Run the v3 checks on the package.
|
||||
for (Workspace& ws : workspaces) {
|
||||
if (auto err{PackageV3Checks(ws.m_ptx, ws.m_vsize, txns, ws.m_ancestors)}) {
|
||||
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "v3-violation", err.value());
|
||||
return PackageMempoolAcceptResult(package_state, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions must meet two minimum feerates: the mempool minimum fee and min relay fee.
|
||||
// For transactions consisting of exactly one child and its parents, it suffices to use the
|
||||
// package feerate (total modified fees / total virtual size) to check this requirement.
|
||||
|
|
|
|||
|
|
@ -419,10 +419,8 @@ class BIP68Test(BitcoinTestFramework):
|
|||
# Use self.nodes[1] to test that version 2 transactions are standard.
|
||||
def test_version2_relay(self):
|
||||
mini_wallet = MiniWallet(self.nodes[1])
|
||||
mini_wallet.rescan_utxos()
|
||||
tx = mini_wallet.create_self_transfer()["tx"]
|
||||
tx.nVersion = 2
|
||||
mini_wallet.sendrawtransaction(from_node=self.nodes[1], tx_hex=tx.serialize().hex())
|
||||
mini_wallet.send_self_transfer(from_node=self.nodes[1], version=2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
BIP68Test().main()
|
||||
|
|
|
|||
419
test/functional/mempool_accept_v3.py
Executable file
419
test/functional/mempool_accept_v3.py
Executable file
|
|
@ -0,0 +1,419 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2024 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 test_framework.messages import (
|
||||
MAX_BIP125_RBF_SEQUENCE,
|
||||
)
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_greater_than_or_equal,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
from test_framework.wallet import (
|
||||
DEFAULT_FEE,
|
||||
MiniWallet,
|
||||
)
|
||||
|
||||
def cleanup(extra_args=None):
|
||||
def decorator(func):
|
||||
def wrapper(self):
|
||||
try:
|
||||
if extra_args is not None:
|
||||
self.restart_node(0, extra_args=extra_args)
|
||||
func(self)
|
||||
finally:
|
||||
# Clear mempool again after test
|
||||
self.generate(self.nodes[0], 1)
|
||||
if extra_args is not None:
|
||||
self.restart_node(0)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
class MempoolAcceptV3(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
self.extra_args = [["-acceptnonstdtxn=1"]]
|
||||
self.setup_clean_chain = True
|
||||
|
||||
def check_mempool(self, txids):
|
||||
"""Assert exact contents of the node's mempool (by txid)."""
|
||||
mempool_contents = self.nodes[0].getrawmempool()
|
||||
assert_equal(len(txids), len(mempool_contents))
|
||||
assert all([txid in txids for txid in mempool_contents])
|
||||
|
||||
@cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
|
||||
def test_v3_acceptance(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test a child of a v3 transaction cannot be more than 1000vB")
|
||||
tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3)
|
||||
self.check_mempool([tx_v3_parent_normal["txid"]])
|
||||
tx_v3_child_heavy = self.wallet.create_self_transfer(
|
||||
utxo_to_spend=tx_v3_parent_normal["new_utxo"],
|
||||
target_weight=4004,
|
||||
version=3
|
||||
)
|
||||
print(tx_v3_child_heavy["tx"].get_vsize())
|
||||
assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), 1000)
|
||||
expected_error_child_heavy = f"v3-rule-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big"
|
||||
assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"])
|
||||
self.check_mempool([tx_v3_parent_normal["txid"]])
|
||||
# tx has no descendants
|
||||
assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1)
|
||||
|
||||
self.log.info("Test that, during replacements, only the new transaction counts for v3 descendant limit")
|
||||
tx_v3_child_almost_heavy = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE,
|
||||
utxo_to_spend=tx_v3_parent_normal["new_utxo"],
|
||||
target_weight=3976, # ELEMENTS
|
||||
version=3
|
||||
)
|
||||
assert_greater_than_or_equal(1000, tx_v3_child_almost_heavy["tx"].get_vsize())
|
||||
self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]])
|
||||
assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
|
||||
tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE * 2,
|
||||
utxo_to_spend=tx_v3_parent_normal["new_utxo"],
|
||||
target_weight=3500,
|
||||
version=3
|
||||
)
|
||||
assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(), 1000)
|
||||
self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]])
|
||||
assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
|
||||
|
||||
@cleanup(extra_args=["-acceptnonstdtxn=1"])
|
||||
def test_v3_replacement(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test v3 transactions may be replaced by v3 transactions")
|
||||
utxo_v3_bip125 = self.wallet.get_utxo()
|
||||
tx_v3_bip125 = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE,
|
||||
utxo_to_spend=utxo_v3_bip125,
|
||||
sequence=MAX_BIP125_RBF_SEQUENCE,
|
||||
version=3
|
||||
)
|
||||
self.check_mempool([tx_v3_bip125["txid"]])
|
||||
|
||||
tx_v3_bip125_rbf = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE * 2,
|
||||
utxo_to_spend=utxo_v3_bip125,
|
||||
version=3
|
||||
)
|
||||
self.check_mempool([tx_v3_bip125_rbf["txid"]])
|
||||
|
||||
self.log.info("Test v3 transactions may be replaced by V2 transactions")
|
||||
tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE * 3,
|
||||
utxo_to_spend=utxo_v3_bip125,
|
||||
version=2
|
||||
)
|
||||
self.check_mempool([tx_v3_bip125_rbf_v2["txid"]])
|
||||
|
||||
self.log.info("Test that replacements cannot cause violation of inherited v3")
|
||||
utxo_v3_parent = self.wallet.get_utxo()
|
||||
tx_v3_parent = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE,
|
||||
utxo_to_spend=utxo_v3_parent,
|
||||
version=3
|
||||
)
|
||||
tx_v3_child = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE,
|
||||
utxo_to_spend=tx_v3_parent["new_utxo"],
|
||||
version=3
|
||||
)
|
||||
self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])
|
||||
|
||||
tx_v3_child_rbf_v2 = self.wallet.create_self_transfer(
|
||||
fee_rate=DEFAULT_FEE * 2,
|
||||
utxo_to_spend=tx_v3_parent["new_utxo"],
|
||||
version=2
|
||||
)
|
||||
expected_error_v2_v3 = f"v3-rule-violation, non-v3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})"
|
||||
assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"])
|
||||
self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])
|
||||
|
||||
|
||||
@cleanup(extra_args=["-acceptnonstdtxn=1"])
|
||||
def test_v3_bip125(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test v3 transactions that don't signal BIP125 are replaceable")
|
||||
assert_equal(node.getmempoolinfo()["fullrbf"], False)
|
||||
utxo_v3_no_bip125 = self.wallet.get_utxo()
|
||||
tx_v3_no_bip125 = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE,
|
||||
utxo_to_spend=utxo_v3_no_bip125,
|
||||
sequence=MAX_BIP125_RBF_SEQUENCE + 1,
|
||||
version=3
|
||||
)
|
||||
|
||||
self.check_mempool([tx_v3_no_bip125["txid"]])
|
||||
assert not node.getmempoolentry(tx_v3_no_bip125["txid"])["bip125-replaceable"]
|
||||
tx_v3_no_bip125_rbf = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
fee_rate=DEFAULT_FEE * 2,
|
||||
utxo_to_spend=utxo_v3_no_bip125,
|
||||
version=3
|
||||
)
|
||||
self.check_mempool([tx_v3_no_bip125_rbf["txid"]])
|
||||
|
||||
@cleanup(extra_args=["-datacarriersize=40000", "-acceptnonstdtxn=1"])
|
||||
def test_v3_reorg(self):
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test that, during a reorg, v3 rules are not enforced")
|
||||
tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2)
|
||||
tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3)
|
||||
tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3)
|
||||
self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]])
|
||||
|
||||
block = self.generate(node, 1)
|
||||
self.check_mempool([])
|
||||
tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
|
||||
tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
|
||||
tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_weight=5000, version=3)
|
||||
assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], 1000)
|
||||
self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]])
|
||||
node.invalidateblock(block[0])
|
||||
self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]])
|
||||
# This is needed because generate() will create the exact same block again.
|
||||
node.reconsiderblock(block[0])
|
||||
|
||||
|
||||
@cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"])
|
||||
def test_nondefault_package_limits(self):
|
||||
"""
|
||||
Max standard tx size + v3 rules imply the ancestor/descendant rules (at their default
|
||||
values), but those checks must not be skipped. Ensure both sets of checks are done by
|
||||
changing the ancestor/descendant limit configurations.
|
||||
"""
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test that a decreased limitdescendantsize also applies to v3 child")
|
||||
tx_v3_parent_large1 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3)
|
||||
tx_v3_child_large1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large1["new_utxo"], version=3)
|
||||
# Child is within v3 limits, but parent's descendant limit is exceeded
|
||||
assert_greater_than(1000, tx_v3_child_large1["tx"].get_vsize())
|
||||
assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds descendant size limit for tx {tx_v3_parent_large1['txid']}", node.sendrawtransaction, tx_v3_child_large1["hex"])
|
||||
self.check_mempool([tx_v3_parent_large1["txid"]])
|
||||
assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1)
|
||||
self.generate(node, 1)
|
||||
|
||||
self.log.info("Test that a decreased limitancestorsize also applies to v3 parent")
|
||||
self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"])
|
||||
tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3)
|
||||
tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], version=3)
|
||||
# Child is within v3 limits
|
||||
assert_greater_than_or_equal(1000, tx_v3_child_large2["tx"].get_vsize())
|
||||
assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds ancestor size limit", node.sendrawtransaction, tx_v3_child_large2["hex"])
|
||||
self.check_mempool([tx_v3_parent_large2["txid"]])
|
||||
|
||||
@cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
|
||||
def test_v3_ancestors_package(self):
|
||||
self.log.info("Test that v3 ancestor limits are checked within the package")
|
||||
node = self.nodes[0]
|
||||
tx_v3_parent_normal = self.wallet.create_self_transfer(
|
||||
fee_rate=0,
|
||||
target_weight=4004,
|
||||
version=3
|
||||
)
|
||||
tx_v3_parent_2_normal = self.wallet.create_self_transfer(
|
||||
fee_rate=0,
|
||||
target_weight=4004,
|
||||
version=3
|
||||
)
|
||||
tx_v3_child_multiparent = self.wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[tx_v3_parent_normal["new_utxo"], tx_v3_parent_2_normal["new_utxo"]],
|
||||
fee_per_output=10000,
|
||||
version=3
|
||||
)
|
||||
tx_v3_child_heavy = self.wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[tx_v3_parent_normal["new_utxo"]],
|
||||
target_weight=4004,
|
||||
fee_per_output=10000,
|
||||
version=3
|
||||
)
|
||||
|
||||
self.check_mempool([])
|
||||
result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]])
|
||||
assert_equal(result['package_msg'], f"v3-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors")
|
||||
self.check_mempool([])
|
||||
|
||||
self.check_mempool([])
|
||||
result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]])
|
||||
# tx_v3_child_heavy is heavy based on weight, not sigops.
|
||||
assert_equal(result['package_msg'], f"v3-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes")
|
||||
self.check_mempool([])
|
||||
|
||||
tx_v3_parent = self.wallet.create_self_transfer(version=3)
|
||||
tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3)
|
||||
tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3)
|
||||
result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]])
|
||||
assert all([txresult["package-error"] == f"v3-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors" for txresult in result])
|
||||
|
||||
@cleanup(extra_args=["-acceptnonstdtxn=1"])
|
||||
def test_v3_ancestors_package_and_mempool(self):
|
||||
"""
|
||||
A v3 transaction in a package cannot have 2 v3 parents.
|
||||
Test that if we have a transaction graph A -> B -> C, where A, B, C are
|
||||
all v3 transactions, that we cannot use submitpackage to get the
|
||||
transactions all into the mempool.
|
||||
|
||||
Verify, in particular, that if A is already in the mempool, then
|
||||
submitpackage(B, C) will fail.
|
||||
"""
|
||||
node = self.nodes[0]
|
||||
self.log.info("Test that v3 ancestor limits include transactions within the package and all in-mempool ancestors")
|
||||
# This is our transaction "A":
|
||||
tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3)
|
||||
|
||||
# Verify that A is in the mempool
|
||||
self.check_mempool([tx_in_mempool["txid"]])
|
||||
|
||||
# tx_0fee_parent is our transaction "B"; just create it.
|
||||
tx_0fee_parent = self.wallet.create_self_transfer(utxo_to_spend=tx_in_mempool["new_utxo"], fee=0, fee_rate=0, version=3)
|
||||
|
||||
# tx_child_violator is our transaction "C"; create it:
|
||||
tx_child_violator = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"]], version=3)
|
||||
|
||||
# submitpackage(B, C) should fail
|
||||
result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]])
|
||||
assert_equal(result['package_msg'], f"v3-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors")
|
||||
self.check_mempool([tx_in_mempool["txid"]])
|
||||
|
||||
@cleanup(extra_args=["-acceptnonstdtxn=1"])
|
||||
def test_mempool_sibling(self):
|
||||
self.log.info("Test that v3 transaction cannot have mempool siblings")
|
||||
node = self.nodes[0]
|
||||
# Add a parent + child to mempool
|
||||
tx_mempool_parent = self.wallet.send_self_transfer_multi(
|
||||
from_node=node,
|
||||
utxos_to_spend=[self.wallet.get_utxo()],
|
||||
num_outputs=2,
|
||||
version=3
|
||||
)
|
||||
tx_mempool_sibling = self.wallet.send_self_transfer(
|
||||
from_node=node,
|
||||
utxo_to_spend=tx_mempool_parent["new_utxos"][0],
|
||||
version=3
|
||||
)
|
||||
self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]])
|
||||
|
||||
tx_has_mempool_sibling = self.wallet.create_self_transfer(
|
||||
utxo_to_spend=tx_mempool_parent["new_utxos"][1],
|
||||
version=3
|
||||
)
|
||||
expected_error_mempool_sibling = f"v3-rule-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit"
|
||||
assert_raises_rpc_error(-26, expected_error_mempool_sibling, node.sendrawtransaction, tx_has_mempool_sibling["hex"])
|
||||
|
||||
tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_has_mempool_sibling["new_utxo"], version=3)
|
||||
|
||||
# Also fails with another non-related transaction via testmempoolaccept
|
||||
tx_unrelated = self.wallet.create_self_transfer(version=3)
|
||||
result_test_unrelated = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_unrelated["hex"]])
|
||||
assert_equal(result_test_unrelated[0]["reject-reason"], "v3-rule-violation")
|
||||
|
||||
result_test_1p1c = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]])
|
||||
assert_equal(result_test_1p1c[0]["reject-reason"], "v3-rule-violation")
|
||||
|
||||
# Also fails with a child via submitpackage
|
||||
result_submitpackage = node.submitpackage([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]])
|
||||
assert_equal(result_submitpackage["tx-results"][tx_has_mempool_sibling['wtxid']]['error'], expected_error_mempool_sibling)
|
||||
|
||||
|
||||
@cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
|
||||
def test_v3_package_inheritance(self):
|
||||
self.log.info("Test that v3 inheritance is checked within package")
|
||||
node = self.nodes[0]
|
||||
tx_v3_parent = self.wallet.create_self_transfer(
|
||||
fee_rate=0,
|
||||
target_weight=4004,
|
||||
version=3
|
||||
)
|
||||
tx_v2_child = self.wallet.create_self_transfer_multi(
|
||||
utxos_to_spend=[tx_v3_parent["new_utxo"]],
|
||||
fee_per_output=10000,
|
||||
version=2
|
||||
)
|
||||
self.check_mempool([])
|
||||
result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]])
|
||||
assert_equal(result['package_msg'], f"v3-violation, non-v3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})")
|
||||
self.check_mempool([])
|
||||
|
||||
@cleanup(extra_args=["-acceptnonstdtxn=1"])
|
||||
def test_v3_in_testmempoolaccept(self):
|
||||
node = self.nodes[0]
|
||||
|
||||
self.log.info("Test that v3 inheritance is accurately assessed in testmempoolaccept")
|
||||
tx_v2 = self.wallet.create_self_transfer(version=2)
|
||||
tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2)
|
||||
tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3)
|
||||
tx_v3 = self.wallet.create_self_transfer(version=3)
|
||||
tx_v2_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=2)
|
||||
tx_v3_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=3)
|
||||
|
||||
# testmempoolaccept paths don't require child-with-parents topology. Ensure that topology
|
||||
# assumptions aren't made in inheritance checks.
|
||||
test_accept_v2_and_v3 = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"]])
|
||||
assert all([result["allowed"] for result in test_accept_v2_and_v3])
|
||||
|
||||
test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]])
|
||||
expected_error_v3_from_v2 = f"v3-violation, v3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-v3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})"
|
||||
assert all([result["package-error"] == expected_error_v3_from_v2 for result in test_accept_v3_from_v2])
|
||||
|
||||
test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]])
|
||||
expected_error_v2_from_v3 = f"v3-violation, non-v3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from v3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})"
|
||||
assert all([result["package-error"] == expected_error_v2_from_v3 for result in test_accept_v2_from_v3])
|
||||
|
||||
test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]])
|
||||
assert all([result["allowed"] for result in test_accept_pairs])
|
||||
|
||||
self.log.info("Test that descendant violations are caught in testmempoolaccept")
|
||||
tx_v3_independent = self.wallet.create_self_transfer(version=3)
|
||||
tx_v3_parent = self.wallet.create_self_transfer_multi(num_outputs=2, version=3)
|
||||
tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3)
|
||||
tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3)
|
||||
test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
|
||||
expected_error_2children = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
|
||||
assert all([result["package-error"] == expected_error_2children for result in test_accept_2children])
|
||||
|
||||
# Extra v3 transaction does not get incorrectly marked as extra descendant
|
||||
test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]])
|
||||
assert all([result["allowed"] for result in test_accept_1child_with_exra])
|
||||
|
||||
# Extra v3 transaction does not make us ignore the extra descendant
|
||||
test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]])
|
||||
expected_error_extra = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
|
||||
assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_exra])
|
||||
# Same result if the parent is already in mempool
|
||||
node.sendrawtransaction(tx_v3_parent["hex"])
|
||||
test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
|
||||
assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_in_mempool_parent])
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("Generate blocks to create UTXOs")
|
||||
node = self.nodes[0]
|
||||
self.wallet = MiniWallet(node)
|
||||
self.generate(self.wallet, 110)
|
||||
self.test_v3_acceptance()
|
||||
self.test_v3_replacement()
|
||||
self.test_v3_bip125()
|
||||
self.test_v3_reorg()
|
||||
self.test_nondefault_package_limits()
|
||||
self.test_v3_ancestors_package()
|
||||
self.test_v3_ancestors_package_and_mempool()
|
||||
self.test_mempool_sibling()
|
||||
self.test_v3_package_inheritance()
|
||||
self.test_v3_in_testmempoolaccept()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MempoolAcceptV3().main()
|
||||
|
|
@ -29,7 +29,7 @@ def check_package_limits(func):
|
|||
testres_error_expected = node.testmempoolaccept(rawtxs=package_hex)
|
||||
assert_equal(len(testres_error_expected), len(package_hex))
|
||||
for txres in testres_error_expected:
|
||||
assert_equal(txres["package-error"], "package-mempool-limits")
|
||||
assert "package-mempool-limits" in txres["package-error"]
|
||||
|
||||
# Clear mempool and check that the package passes now
|
||||
self.generate(node, 1)
|
||||
|
|
|
|||
|
|
@ -166,7 +166,8 @@ class BytesPerSigOpTest(BitcoinTestFramework):
|
|||
# But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked
|
||||
# here, it would get further in validation and give too-long-mempool-chain error instead.
|
||||
packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()])
|
||||
assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"])
|
||||
expected_package_error = f"package-mempool-limits, package size {2*20*5000} exceeds ancestor size limit [limit: 101000]"
|
||||
assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * 2)
|
||||
|
||||
# When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits
|
||||
res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()])
|
||||
|
|
|
|||
|
|
@ -272,14 +272,8 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
|
|||
# Create a free transaction. Should be rejected.
|
||||
tx_res = self.wallet.create_self_transfer(fee_rate=0)
|
||||
|
||||
# ELEMENTS: create_self_trasfer adds a fee output at the end of the transaction
|
||||
# ELEMENTS: cannot have a 0 fee output (unlike bitcoin), so we need to set it
|
||||
# manually to the lowest possible value (1 satoshi)
|
||||
# ELEMENTS: for zero fee fee output is not added
|
||||
tx = tx_res['tx']
|
||||
assert tx.vout[1].is_fee()
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() - 1)
|
||||
tx.vout[1].nValue.setToAmount(1)
|
||||
|
||||
tx_hex = tx.serialize().hex()
|
||||
tx_id = tx.rehash()
|
||||
|
||||
|
|
@ -296,7 +290,7 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
|
|||
self.log.info("Assert that prioritised free transaction is accepted to mempool")
|
||||
assert_equal(self.nodes[0].sendrawtransaction(tx_hex), tx_id)
|
||||
assert tx_id in self.nodes[0].getrawmempool()
|
||||
assert_equal(self.nodes[0].getprioritisedtransactions()[tx_id], { "fee_delta" : self.relayfee*COIN, "in_mempool" : True, "modified_fee": int(self.relayfee*COIN + COIN * tx_res["fee"] + 1)}) # ELEMENTS
|
||||
assert_equal(self.nodes[0].getprioritisedtransactions()[tx_id], { "fee_delta" : self.relayfee*COIN, "in_mempool" : True, "modified_fee": int(self.relayfee*COIN + COIN * tx_res["fee"])}) # ELEMENTS
|
||||
|
||||
# Test that calling prioritisetransaction is sufficient to trigger
|
||||
# getblocktemplate to (eventually) return a new block.
|
||||
|
|
|
|||
|
|
@ -264,9 +264,9 @@ class MiniWallet:
|
|||
available that can cover the cost for the amount and the fixed fee
|
||||
(the utxo with the largest value is taken).
|
||||
"""
|
||||
tx = self.create_self_transfer(fee_rate=0)['tx']
|
||||
tx = self.create_self_transfer(fee_rate=0, fee=Decimal(fee/COIN).quantize(Decimal('1e-8')))['tx']
|
||||
assert_greater_than_or_equal(tx.vout[0].nValue.getAmount(), amount + fee)
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() - (amount + fee)) # change output -> MiniWallet
|
||||
tx.vout[0].nValue.setToAmount(tx.vout[0].nValue.getAmount() - amount) # change output -> MiniWallet
|
||||
tx.vout[1].nValue.setToAmount(fee) # ELEMENTS explicitly set fee output value
|
||||
tx.vout.append(CTxOut(amount, scriptPubKey)) # arbitrary output -> to be returned
|
||||
txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex())
|
||||
|
|
@ -291,6 +291,7 @@ class MiniWallet:
|
|||
utxos_to_spend: Optional[list[dict]] = None,
|
||||
num_outputs=1,
|
||||
amount_per_output=0,
|
||||
version=2,
|
||||
locktime=0,
|
||||
sequence=0,
|
||||
fee_per_output=1000,
|
||||
|
|
@ -319,7 +320,7 @@ class MiniWallet:
|
|||
# add it to the fee output
|
||||
extra_output_value = outputs_value_total % num_outputs
|
||||
|
||||
# ELEMENTS: add fee output as final output
|
||||
# ELEMENTS: add fee output as final output if non-zero
|
||||
fee = inputs_value_total - outputs_value_total + extra_output_value
|
||||
assert fee >= 0
|
||||
|
||||
|
|
@ -327,7 +328,9 @@ class MiniWallet:
|
|||
tx = CTransaction()
|
||||
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)]
|
||||
tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)]
|
||||
tx.vout.append(CTxOut(nValue=CTxOutValue(fee)))
|
||||
if fee != 0:
|
||||
tx.vout.append(CTxOut(nValue=CTxOutValue(fee)))
|
||||
tx.nVersion = version
|
||||
tx.nLockTime = locktime
|
||||
|
||||
self.sign_tx(tx)
|
||||
|
|
@ -344,7 +347,7 @@ class MiniWallet:
|
|||
height=0,
|
||||
coinbase=False,
|
||||
confirmations=0,
|
||||
) for i in range(len(tx.vout) - 1)],
|
||||
) for i in range(len(tx.vout) - ( 1 if fee else 0 ))], # ELEMENTS remove fee output only if fee is non-zero
|
||||
"fee": fee,
|
||||
"txid": txid,
|
||||
"wtxid": tx.getwtxid(),
|
||||
|
|
@ -352,14 +355,15 @@ class MiniWallet:
|
|||
"tx": tx,
|
||||
}
|
||||
|
||||
def create_self_transfer(self, *,
|
||||
def create_self_transfer(
|
||||
self,
|
||||
*,
|
||||
fee_rate=Decimal("0.003"),
|
||||
fee=Decimal("0"),
|
||||
utxo_to_spend=None,
|
||||
locktime=0,
|
||||
sequence=0,
|
||||
target_weight=0,
|
||||
confirmed_only=False
|
||||
confirmed_only=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed."""
|
||||
utxo_to_spend = utxo_to_spend or self.get_utxo(confirmed_only=confirmed_only)
|
||||
|
|
@ -372,14 +376,23 @@ class MiniWallet:
|
|||
vsize = Decimal(248) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
|
||||
else:
|
||||
assert False
|
||||
# ELEMENTS: no fee output if fee == 0
|
||||
if fee == 0 and fee_rate == 0:
|
||||
vsize = vsize - 44
|
||||
send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000))
|
||||
total_input_value_sats = int(COIN * utxo_to_spend["value"])
|
||||
send_value_sats = int(COIN * send_value)
|
||||
fee_sats = total_input_value_sats - send_value_sats
|
||||
|
||||
|
||||
# create tx
|
||||
tx = self.create_self_transfer_multi(utxos_to_spend=[utxo_to_spend], locktime=locktime, sequence=sequence, amount_per_output=int(COIN * send_value), target_weight=target_weight, fee=fee_sats)
|
||||
tx = self.create_self_transfer_multi(
|
||||
utxos_to_spend=[utxo_to_spend],
|
||||
amount_per_output=int(COIN * send_value),
|
||||
target_weight=target_weight,
|
||||
fee=fee_sats, # ELEMENTS
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if not target_weight:
|
||||
assert_approx(tx["tx"].get_vsize(), vsize, 1) # ELEMENTS FIXME: feature_cltv is 184 instead of 185
|
||||
tx["new_utxo"] = tx.pop("new_utxos")[0]
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ BASE_SCRIPTS = [
|
|||
'p2p_v2_encrypted.py',
|
||||
'p2p_v2_earlykeyresponse.py',
|
||||
'example_test.py',
|
||||
'mempool_accept_v3.py',
|
||||
'wallet_txn_doublespend.py --legacy-wallet',
|
||||
'wallet_multisig_descriptor_psbt.py --descriptors',
|
||||
'wallet_txn_doublespend.py --descriptors',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue