Skip to content

Commit 2303fd2

Browse files
committed
Merge bitcoin/bitcoin#28471: Fix virtual size limit enforcement in transaction package context
eb8f58f Add functional test to catch too large vsize packages (Greg Sanders) 1a579f9 Handle over-sized (in virtual bytes) packages with no in-mempool ancestors (Greg Sanders) bc013fe Bugfix: Pass correct virtual size to CheckPackageLimits (Luke Dashjr) 533660c Replace MAX_PACKAGE_SIZE with MAX_PACKAGE_WEIGHT to avoid vbyte confusion (Greg Sanders) Pull request description: (Alternative) Minimal subset of bitcoin/bitcoin#28345 to: 1) Replace MAX_PACKAGE_SIZE with MAX_PACKAGE_WEIGHT which accounts for additional WU necessary to not exclude default chain limit transactions that would have been accepted individually. Avoids sigops vbyte confusion. 2) pass correct vsize to chain limit evaluations in package context 3) stop overly-large packages that have no existing mempool ancestors (also a bugfix by itself if someone sets non-standard chain limits) This should fix the known issues while not blocking additional refactoring later. ACKs for top commit: achow101: ACK eb8f58f ariard: Re-Code ACK eb8f58f glozow: reACK eb8f58f Tree-SHA512: 1b5cca1a526207e25d387fcc29a776a3198c3a013dc2b35c6275b9d5a64db2476c154ebf52e3a1aed0b9924c75613f21a946577aa760de28cadf0c9c7f68dc39
2 parents cf0711c + eb8f58f commit 2303fd2

File tree

8 files changed

+99
-28
lines changed

8 files changed

+99
-28
lines changed

doc/policy/packages.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,19 @@ tip or some preceding transaction in the package.
1818

1919
The following rules are enforced for all packages:
2020

21-
* Packages cannot exceed `MAX_PACKAGE_COUNT=25` count and `MAX_PACKAGE_SIZE=101KvB` total size
21+
* Packages cannot exceed `MAX_PACKAGE_COUNT=25` count and `MAX_PACKAGE_WEIGHT=404000` total weight
2222
(#20833)
2323

24-
- *Rationale*: This is already enforced as mempool ancestor/descendant limits. If
25-
transactions in a package are all related, exceeding this limit would mean that the package
26-
can either be split up or it wouldn't pass individual mempool policy.
24+
- *Rationale*: We want package size to be as small as possible to mitigate DoS via package
25+
validation. However, we want to make sure that the limit does not restrict ancestor
26+
packages that would be allowed if submitted individually.
2727

2828
- Note that, if these mempool limits change, package limits should be reconsidered. Users may
2929
also configure their mempool limits differently.
3030

31+
- Note that the this is transaction weight, not "virtual" size as with other limits to allow
32+
simpler context-less checks.
33+
3134
* Packages must be topologically sorted. (#20833)
3235

3336
* Packages cannot have conflicting transactions, i.e. no two transactions in a package can spend

src/policy/packages.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ bool CheckPackage(const Package& txns, PackageValidationState& state)
2323
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-many-transactions");
2424
}
2525

26-
const int64_t total_size = std::accumulate(txns.cbegin(), txns.cend(), 0,
27-
[](int64_t sum, const auto& tx) { return sum + GetVirtualTransactionSize(*tx); });
28-
// If the package only contains 1 tx, it's better to report the policy violation on individual tx size.
29-
if (package_count > 1 && total_size > MAX_PACKAGE_SIZE * 1000) {
26+
const int64_t total_weight = std::accumulate(txns.cbegin(), txns.cend(), 0,
27+
[](int64_t sum, const auto& tx) { return sum + GetTransactionWeight(*tx); });
28+
// If the package only contains 1 tx, it's better to report the policy violation on individual tx weight.
29+
if (package_count > 1 && total_weight > MAX_PACKAGE_WEIGHT) {
3030
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-large");
3131
}
3232

src/policy/packages.h

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@
1515

1616
/** Default maximum number of transactions in a package. */
1717
static constexpr uint32_t MAX_PACKAGE_COUNT{25};
18-
/** Default maximum total virtual size of transactions in a package in KvB. */
19-
static constexpr uint32_t MAX_PACKAGE_SIZE{101};
20-
static_assert(MAX_PACKAGE_SIZE * WITNESS_SCALE_FACTOR * 1000 >= MAX_STANDARD_TX_WEIGHT);
18+
/** Default maximum total weight of transactions in a package in weight
19+
to allow for context-less checks. This must allow a superset of sigops
20+
weighted vsize limited transactions to not disallow transactions we would
21+
have otherwise accepted individually. */
22+
static constexpr uint32_t MAX_PACKAGE_WEIGHT = 404'000;
23+
static_assert(MAX_PACKAGE_WEIGHT >= MAX_STANDARD_TX_WEIGHT);
2124

22-
// If a package is submitted, it must be within the mempool's ancestor/descendant limits. Since a
23-
// submitted package must be child-with-unconfirmed-parents (all of the transactions are an ancestor
25+
// If a package is to be evaluated, it must be at least as large as the mempool's ancestor/descendant limits,
26+
// otherwise transactions that would be individually accepted may be rejected in a package erroneously.
27+
// Since a submitted package must be child-with-unconfirmed-parents (all of the transactions are an ancestor
2428
// of the child), package limits are ultimately bounded by mempool package limits. Ensure that the
2529
// defaults reflect this constraint.
2630
static_assert(DEFAULT_DESCENDANT_LIMIT >= MAX_PACKAGE_COUNT);
2731
static_assert(DEFAULT_ANCESTOR_LIMIT >= MAX_PACKAGE_COUNT);
28-
static_assert(DEFAULT_ANCESTOR_SIZE_LIMIT_KVB >= MAX_PACKAGE_SIZE);
29-
static_assert(DEFAULT_DESCENDANT_SIZE_LIMIT_KVB >= MAX_PACKAGE_SIZE);
32+
static_assert(MAX_PACKAGE_WEIGHT >= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * WITNESS_SCALE_FACTOR * 1000);
33+
static_assert(MAX_PACKAGE_WEIGHT >= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * WITNESS_SCALE_FACTOR * 1000);
3034

3135
/** A "reason" why a package was invalid. It may be that one or more of the included
3236
* transactions is invalid or the package itself violates our rules.
@@ -47,7 +51,7 @@ class PackageValidationState : public ValidationState<PackageValidationResult> {
4751

4852
/** Context-free package policy checks:
4953
* 1. The number of transactions cannot exceed MAX_PACKAGE_COUNT.
50-
* 2. The total virtual size cannot exceed MAX_PACKAGE_SIZE.
54+
* 2. The total weight cannot exceed MAX_PACKAGE_WEIGHT.
5155
* 3. If any dependencies exist between transactions, parents must appear before children.
5256
* 4. Transactions cannot conflict, i.e., spend the same inputs.
5357
*/

src/test/txpackage_tests.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ BOOST_FIXTURE_TEST_CASE(package_sanitization_tests, TestChain100Setup)
5151
BOOST_CHECK_EQUAL(state_too_many.GetResult(), PackageValidationResult::PCKG_POLICY);
5252
BOOST_CHECK_EQUAL(state_too_many.GetRejectReason(), "package-too-many-transactions");
5353

54-
// Packages can't have a total size of more than 101KvB.
54+
// Packages can't have a total weight of more than 404'000WU.
5555
CTransactionRef large_ptx = create_placeholder_tx(150, 150);
5656
Package package_too_large;
57-
auto size_large = GetVirtualTransactionSize(*large_ptx);
58-
size_t total_size{0};
59-
while (total_size <= MAX_PACKAGE_SIZE * 1000) {
57+
auto size_large = GetTransactionWeight(*large_ptx);
58+
size_t total_weight{0};
59+
while (total_weight <= MAX_PACKAGE_WEIGHT) {
6060
package_too_large.push_back(large_ptx);
61-
total_size += size_large;
61+
total_weight += size_large;
6262
}
6363
BOOST_CHECK(package_too_large.size() <= MAX_PACKAGE_COUNT);
6464
PackageValidationState state_too_large;
@@ -122,7 +122,7 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup)
122122

123123
// A single, giant transaction submitted through ProcessNewPackage fails on single tx policy.
124124
CTransactionRef giant_ptx = create_placeholder_tx(999, 999);
125-
BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) > MAX_PACKAGE_SIZE * 1000);
125+
BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) > DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000);
126126
auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, {giant_ptx}, /*test_accept=*/true);
127127
BOOST_CHECK(result_single_large.m_state.IsInvalid());
128128
BOOST_CHECK_EQUAL(result_single_large.m_state.GetResult(), PackageValidationResult::PCKG_TX);

src/txmempool.cpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,28 @@ util::Result<CTxMemPool::setEntries> CTxMemPool::CalculateAncestorsAndCheckLimit
197197
}
198198

199199
bool CTxMemPool::CheckPackageLimits(const Package& package,
200+
const int64_t total_vsize,
200201
std::string &errString) const
201202
{
203+
size_t pack_count = package.size();
204+
205+
// Package itself is busting mempool limits; should be rejected even if no staged_ancestors exist
206+
if (pack_count > static_cast<uint64_t>(m_limits.ancestor_count)) {
207+
errString = strprintf("package count %u exceeds ancestor count limit [limit: %u]", pack_count, m_limits.ancestor_count);
208+
return false;
209+
} else if (pack_count > static_cast<uint64_t>(m_limits.descendant_count)) {
210+
errString = strprintf("package count %u exceeds descendant count limit [limit: %u]", pack_count, m_limits.descendant_count);
211+
return false;
212+
} else if (total_vsize > m_limits.ancestor_size_vbytes) {
213+
errString = strprintf("package size %u exceeds ancestor size limit [limit: %u]", total_vsize, m_limits.ancestor_size_vbytes);
214+
return false;
215+
} else if (total_vsize > m_limits.descendant_size_vbytes) {
216+
errString = strprintf("package size %u exceeds descendant size limit [limit: %u]", total_vsize, m_limits.descendant_size_vbytes);
217+
return false;
218+
}
219+
202220
CTxMemPoolEntry::Parents staged_ancestors;
203-
int64_t total_size = 0;
204221
for (const auto& tx : package) {
205-
total_size += GetVirtualTransactionSize(*tx);
206222
for (const auto& input : tx->vin) {
207223
std::optional<txiter> piter = GetIter(input.prevout.hash);
208224
if (piter) {
@@ -217,7 +233,7 @@ bool CTxMemPool::CheckPackageLimits(const Package& package,
217233
// When multiple transactions are passed in, the ancestors and descendants of all transactions
218234
// considered together must be within limits even if they are not interdependent. This may be
219235
// stricter than the limits for each individual transaction.
220-
const auto ancestors{CalculateAncestorsAndCheckLimits(total_size, package.size(),
236+
const auto ancestors{CalculateAncestorsAndCheckLimits(total_vsize, package.size(),
221237
staged_ancestors, m_limits)};
222238
// It's possible to overestimate the ancestor/descendant totals.
223239
if (!ancestors.has_value()) errString = "possibly " + util::ErrorString(ancestors).original;

src/txmempool.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,9 +606,11 @@ class CTxMemPool
606606
* @param[in] package Transaction package being evaluated for acceptance
607607
* to mempool. The transactions need not be direct
608608
* ancestors/descendants of each other.
609+
* @param[in] total_vsize Sum of virtual sizes for all transactions in package.
609610
* @param[out] errString Populated with error reason if a limit is hit.
610611
*/
611612
bool CheckPackageLimits(const Package& package,
613+
int64_t total_vsize,
612614
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);
613615

614616
/** Populate setDescendants with all in-mempool descendants of hash.

src/validation.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ class MemPoolAccept
634634
// Enforce package mempool ancestor/descendant limits (distinct from individual
635635
// ancestor/descendant limits done in PreChecks).
636636
bool PackageMempoolChecks(const std::vector<CTransactionRef>& txns,
637+
int64_t total_vsize,
637638
PackageValidationState& package_state) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs);
638639

639640
// Run the script checks using our policy flags. As this can be slow, we should
@@ -1003,6 +1004,7 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
10031004
}
10041005

10051006
bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txns,
1007+
const int64_t total_vsize,
10061008
PackageValidationState& package_state)
10071009
{
10081010
AssertLockHeld(cs_main);
@@ -1013,7 +1015,7 @@ bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txn
10131015
{ return !m_pool.exists(GenTxid::Txid(tx->GetHash()));}));
10141016

10151017
std::string err_string;
1016-
if (!m_pool.CheckPackageLimits(txns, err_string)) {
1018+
if (!m_pool.CheckPackageLimits(txns, total_vsize, err_string)) {
10171019
// This is a package-wide error, separate from an individual transaction error.
10181020
return package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", err_string);
10191021
}
@@ -1298,7 +1300,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
12981300
// because it's unnecessary. Also, CPFP carve out can increase the limit for individual
12991301
// transactions, but this exemption is not extended to packages in CheckPackageLimits().
13001302
std::string err_string;
1301-
if (txns.size() > 1 && !PackageMempoolChecks(txns, package_state)) {
1303+
if (txns.size() > 1 && !PackageMempoolChecks(txns, m_total_vsize, package_state)) {
13021304
return PackageMempoolAcceptResult(package_state, std::move(results));
13031305
}
13041306

test/functional/mempool_sigoplimit.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test sigop limit mempool policy (`-bytespersigop` parameter)"""
6+
from decimal import Decimal
67
from math import ceil
78

89
from test_framework.messages import (
@@ -25,16 +26,18 @@
2526
OP_TRUE,
2627
)
2728
from test_framework.script_util import (
29+
keys_to_multisig_script,
2830
script_to_p2wsh_script,
2931
)
3032
from test_framework.test_framework import BitcoinTestFramework
3133
from test_framework.util import (
3234
assert_equal,
3335
assert_greater_than,
3436
assert_greater_than_or_equal,
37+
assert_raises_rpc_error,
3538
)
3639
from test_framework.wallet import MiniWallet
37-
40+
from test_framework.wallet_util import generate_keypair
3841

3942
DEFAULT_BYTES_PER_SIGOP = 20 # default setting
4043

@@ -133,6 +136,45 @@ def test_sigops_limit(self, bytes_per_sigop, num_sigops):
133136
assert_equal(entry_parent['descendantcount'], 2)
134137
assert_equal(entry_parent['descendantsize'], parent_tx.get_vsize() + sigop_equivalent_vsize)
135138

139+
def test_sigops_package(self):
140+
self.log.info("Test a overly-large sigops-vbyte hits package limits")
141+
# Make a 2-transaction package which fails vbyte checks even though
142+
# separately they would work.
143+
self.restart_node(0, extra_args=["-bytespersigop=5000"] + self.extra_args[0])
144+
145+
def create_bare_multisig_tx(utxo_to_spend=None):
146+
_, pubkey = generate_keypair()
147+
amount_for_bare = 50000
148+
tx_dict = self.wallet.create_self_transfer(fee=Decimal("3"), utxo_to_spend=utxo_to_spend)
149+
tx_utxo = tx_dict["new_utxo"]
150+
tx = tx_dict["tx"]
151+
tx.vout.append(CTxOut(amount_for_bare, keys_to_multisig_script([pubkey], k=1)))
152+
tx.vout[0].nValue -= amount_for_bare
153+
tx_utxo["txid"] = tx.rehash()
154+
tx_utxo["value"] -= Decimal("0.00005000")
155+
return (tx_utxo, tx)
156+
157+
tx_parent_utxo, tx_parent = create_bare_multisig_tx()
158+
tx_child_utxo, tx_child = create_bare_multisig_tx(tx_parent_utxo)
159+
160+
# Separately, the parent tx is ok
161+
parent_individual_testres = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex()])[0]
162+
assert parent_individual_testres["allowed"]
163+
# Multisig is counted as MAX_PUBKEYS_PER_MULTISIG = 20 sigops
164+
assert_equal(parent_individual_testres["vsize"], 5000 * 20)
165+
166+
# But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked
167+
# here, it would get further in validation and give too-long-mempool-chain error instead.
168+
packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()])
169+
assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"])
170+
171+
# When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits
172+
assert_raises_rpc_error(-26, "too-long-mempool-chain", self.nodes[0].submitpackage, [tx_parent.serialize().hex(), tx_child.serialize().hex()])
173+
assert tx_parent.rehash() in self.nodes[0].getrawmempool()
174+
175+
# Transactions are tiny in weight
176+
assert_greater_than(2000, tx_parent.get_weight() + tx_child.get_weight())
177+
136178
def run_test(self):
137179
self.wallet = MiniWallet(self.nodes[0])
138180

@@ -149,6 +191,8 @@ def run_test(self):
149191

150192
self.generate(self.wallet, 1)
151193

194+
self.test_sigops_package()
195+
152196

153197
if __name__ == '__main__':
154198
BytesPerSigOpTest().main()

0 commit comments

Comments
 (0)