Skip to content

Commit

Permalink
Merge bitcoin#28251: validation: fix coins disappearing mid-package e…
Browse files Browse the repository at this point in the history
…valuation

32c1dd1 [test] mempool coins disappearing mid-package evaluation (glozow)
a67f460 [refactor] split setup in mempool_limit test (glozow)
d086961 [test framework] add ability to spend only confirmed utxos (glozow)
3ea71fe [validation] don't LimitMempoolSize in any subpackage submissions (glozow)
d227b72 [validation] return correct result when already-in-mempool tx gets evicted (glozow)
9698b81 [refactor] back-fill results in AcceptPackage (glozow)
8ad7ad3 [validation] make PackageMempoolAcceptResult members mutable (glozow)
03b87c1 [validation] add AcceptSubPackage to delegate Accept* calls and clean up m_view (glozow)
3f01a3d [CCoinsViewMemPool] track non-base coins and allow Reset (glozow)
7d7f7a1 [policy] check for duplicate txids in package (glozow)

Pull request description:

  While we are evaluating a package, we split it into "subpackages" for evaluation (currently subpackages all have size 1 except the last one). If a subpackage has size 1, we may add a tx to mempool and call `LimitMempoolSize()`, which evicts transactions if the mempool gets full. We handle the case where the just-submitted transaction is evicted immediately, but we don't handle the case in which a transaction from a previous subpackage (either just submitted or already in mempool) is evicted. Mainly, since the coins created by the evicted transaction are cached in `m_view`, we don't realize the UTXO has disappeared until `CheckInputsFromMempoolAndCache` asserts that they exist. Also, the returned `PackageMempoolAcceptResult` reports that the transaction is in mempool even though it isn't anymore.

  Fix this by not calling `LimitMempoolSize()` until the very end, and editing the results map with "mempool full" if things fall out.

  Pointed out by instagibbs in bitcoin@faeed68 on top of the v3 PR.

ACKs for top commit:
  instagibbs:
    reACK bitcoin@32c1dd1

Tree-SHA512: 61e7f69db4712e5e5bfa27d037ab66bdd97f1bf60a8d9ffb96adb1f0609af012c810d681102ee5c7baec7b5fe8cb7c304a60c63ccc445d00d86a2b7f0e7ddb90
  • Loading branch information
fanquake committed Sep 13, 2023
2 parents adc0921 + 32c1dd1 commit f1a9fd6
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 95 deletions.
7 changes: 7 additions & 0 deletions src/policy/packages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ bool CheckPackage(const Package& txns, PackageValidationState& state)
std::unordered_set<uint256, SaltedTxidHasher> later_txids;
std::transform(txns.cbegin(), txns.cend(), std::inserter(later_txids, later_txids.end()),
[](const auto& tx) { return tx->GetHash(); });

// Package must not contain any duplicate transactions, which is checked by txid. This also
// includes transactions with duplicate wtxids and same-txid-different-witness transactions.
if (later_txids.size() != txns.size()) {
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-contains-duplicates");
}

for (const auto& tx : txns) {
for (const auto& input : tx->vin) {
if (later_txids.find(input.prevout.hash) != later_txids.end()) {
Expand Down
17 changes: 15 additions & 2 deletions src/test/txpackage_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ BOOST_FIXTURE_TEST_CASE(package_sanitization_tests, TestChain100Setup)
BOOST_CHECK(!CheckPackage(package_too_large, state_too_large));
BOOST_CHECK_EQUAL(state_too_large.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_too_large.GetRejectReason(), "package-too-large");

// Packages can't contain transactions with the same txid.
Package package_duplicate_txids_empty;
for (auto i{0}; i < 3; ++i) {
CMutableTransaction empty_tx;
package_duplicate_txids_empty.emplace_back(MakeTransactionRef(empty_tx));
}
PackageValidationState state_duplicates;
BOOST_CHECK(!CheckPackage(package_duplicate_txids_empty, state_duplicates));
BOOST_CHECK_EQUAL(state_duplicates.GetResult(), PackageValidationResult::PCKG_POLICY);
BOOST_CHECK_EQUAL(state_duplicates.GetRejectReason(), "package-contains-duplicates");
}

BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup)
Expand Down Expand Up @@ -809,18 +820,20 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup)
expected_pool_size += 1;
BOOST_CHECK_MESSAGE(submit_rich_parent.m_state.IsInvalid(), "Package validation unexpectedly succeeded");

// The child would have been validated on its own and failed, then submitted as a "package" of 1.
// The child would have been validated on its own and failed.
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetResult(), PackageValidationResult::PCKG_TX);
BOOST_CHECK_EQUAL(submit_rich_parent.m_state.GetRejectReason(), "transaction failed");

auto it_parent = submit_rich_parent.m_tx_results.find(tx_parent_rich->GetWitnessHash());
auto it_child = submit_rich_parent.m_tx_results.find(tx_child_poor->GetWitnessHash());
BOOST_CHECK(it_parent != submit_rich_parent.m_tx_results.end());
BOOST_CHECK(it_child != submit_rich_parent.m_tx_results.end());
BOOST_CHECK(it_parent->second.m_result_type == MempoolAcceptResult::ResultType::VALID);
BOOST_CHECK(it_child->second.m_result_type == MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK(it_parent->second.m_state.GetRejectReason() == "");
BOOST_CHECK_MESSAGE(it_parent->second.m_base_fees.value() == high_parent_fee,
strprintf("rich parent: expected fee %s, got %s", high_parent_fee, it_parent->second.m_base_fees.value()));
BOOST_CHECK(it_parent->second.m_effective_feerate == CFeeRate(high_parent_fee, GetVirtualTransactionSize(*tx_parent_rich)));
auto it_child = submit_rich_parent.m_tx_results.find(tx_child_poor->GetWitnessHash());
BOOST_CHECK(it_child != submit_rich_parent.m_tx_results.end());
BOOST_CHECK_EQUAL(it_child->second.m_result_type, MempoolAcceptResult::ResultType::INVALID);
BOOST_CHECK_EQUAL(it_child->second.m_state.GetResult(), TxValidationResult::TX_MEMPOOL_POLICY);
Expand Down
7 changes: 7 additions & 0 deletions src/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,7 @@ bool CCoinsViewMemPool::GetCoin(const COutPoint &outpoint, Coin &coin) const {
if (ptx) {
if (outpoint.n < ptx->vout.size()) {
coin = Coin(ptx->vout[outpoint.n], MEMPOOL_HEIGHT, false);
m_non_base_coins.emplace(outpoint);
return true;
} else {
return false;
Expand All @@ -1005,8 +1006,14 @@ void CCoinsViewMemPool::PackageAddTransaction(const CTransactionRef& tx)
{
for (unsigned int n = 0; n < tx->vout.size(); ++n) {
m_temp_added.emplace(COutPoint(tx->GetHash(), n), Coin(tx->vout[n], MEMPOOL_HEIGHT, false));
m_non_base_coins.emplace(COutPoint(tx->GetHash(), n));
}
}
void CCoinsViewMemPool::Reset()
{
m_temp_added.clear();
m_non_base_coins.clear();
}

size_t CTxMemPool::DynamicMemoryUsage() const {
LOCK(cs);
Expand Down
12 changes: 12 additions & 0 deletions src/txmempool.h
Original file line number Diff line number Diff line change
Expand Up @@ -826,15 +826,27 @@ class CCoinsViewMemPool : public CCoinsViewBacked
* validation, since we can access transaction outputs without submitting them to mempool.
*/
std::unordered_map<COutPoint, Coin, SaltedOutpointHasher> m_temp_added;

/**
* Set of all coins that have been fetched from mempool or created using PackageAddTransaction
* (not base). Used to track the origin of a coin, see GetNonBaseCoins().
*/
mutable std::unordered_set<COutPoint, SaltedOutpointHasher> m_non_base_coins;
protected:
const CTxMemPool& mempool;

public:
CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn);
/** GetCoin, returning whether it exists and is not spent. Also updates m_non_base_coins if the
* coin is not fetched from base. */
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
/** Add the coins created by this transaction. These coins are only temporarily stored in
* m_temp_added and cannot be flushed to the back end. Only used for package validation. */
void PackageAddTransaction(const CTransactionRef& tx);
/** Get all coins in m_non_base_coins. */
std::unordered_set<COutPoint, SaltedOutpointHasher> GetNonBaseCoins() const { return m_non_base_coins; }
/** Clear m_temp_added and m_non_base_coins. */
void Reset();
};

/**
Expand Down
Loading

0 comments on commit f1a9fd6

Please sign in to comment.