Skip to content

Commit 7143d43

Browse files
committed
Merge bitcoin/bitcoin#28948: v3 transaction policy for anti-pinning
29029df [doc] v3 signaling in mempool-replacements.md (glozow) e643ea7 [fuzz] v3 transactions and sigop-adjusted vsize (glozow) 1fd16b5 [functional test] v3 transaction submission (glozow) 27c8786 test framework: Add and use option for tx-version in MiniWallet methods (MarcoFalke) 9a1fea5 [policy/validation] allow v3 transactions with certain restrictions (glozow) eb8d5a2 [policy] add v3 policy rules (glozow) 9a29d47 [rpc] return full string for package_msg and package-error (glozow) 158623b [refactor] change Workspace::m_conflicts and adjacent funcs/structs to use Txid (glozow) Pull request description: See #27463 for overall package relay tracking. Delving Bitcoin discussion thread: https://delvingbitcoin.org/t/v3-transaction-policy-for-anti-pinning/340 Delving Bitcoin discussion for LN usage: https://delvingbitcoin.org/t/lightning-transactions-with-v3-and-ephemeral-anchors/418 Rationale: - There are various pinning problems with RBF and our general ancestor/descendant limits. These policies help mitigate many pinning attacks and make package RBF feasible (see #28984 which implements package RBF on top of this). I would focus the most here on Rule 3 pinning. [1][2] - Switching to a cluster-based mempool (see #27677 and #28676) requires the removal of CPFP carve out, which applications depend on. V3 + package RBF + ephemeral anchors + 1-parent-1-child package relay provides an intermediate solution. V3 policy is for "Priority Transactions." [3][4] It allows users to opt in to more restrictive topological limits for shared transactions, in exchange for the more robust fee-bumping abilities that offers. Even though we don't have cluster limits, we are able to treat these transactions as having as having a maximum cluster size of 2. Immediate benefits: - You can presign a transaction with 0 fees (not just 1sat/vB!) and add a fee-bump later. - Rule 3 pinning is reduced by a significant amount, since the attacker can only attach a maximum of 1000vB to your shared transaction. This also enables some other cool things (again see #27463 for overall roadmap): - Ephemeral Anchors - Package RBF for these 1-parent-1-child packages. That means e.g. a commitment tx + child can replace another commitment tx using the child's fees. - We can transition to a "single anchor" universe without worrying about package limit pinning. So current users of CPFP carve out would have something else to use. - We can switch to a cluster-based mempool [5] (#27677 #28676), which removes CPFP carve out [6]. [1]: Original mailing list post and discussion about RBF pinning problems https://gist.github.com/glozow/25d9662c52453bd08b4b4b1d3783b9ff, https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-January/019817.html [2]: A FAQ is "we need this for cluster mempool, but is this still necessary afterwards?" There are some pinning issues that are fixed here and not fully fixed in cluster mempool, so we will still want this or something similar afterward. [3]: Mailing list post for v3 https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-September/020937.html [4]: Original PR #25038 also contains a lot of the discussion [5]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393/7 [6]: https://delvingbitcoin.org/t/an-overview-of-the-cluster-mempool-proposal/393#the-cpfp-carveout-rule-can-no-longer-be-supported-12 ACKs for top commit: sdaftuar: ACK 29029df achow101: ACK 29029df instagibbs: ACK 29029df modulo that Tree-SHA512: 9664b078890cfdca2a146439f8835c9d9ab483f43b30af8c7cd6962f09aa557fb1ce7689d5e130a2ec142235dbc8f21213881baa75241c5881660f9008d68450
2 parents 1d334d8 + 29029df commit 7143d43

23 files changed

+1136
-42
lines changed

doc/policy/mempool-replacements.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ their in-mempool descendants (together, "original transactions") if, in addition
1111
other consensus and policy rules, each of the following conditions are met:
1212

1313
1. The directly conflicting transactions all signal replaceability explicitly. A transaction is
14-
signaling replaceability if any of its inputs have an nSequence number less than (0xffffffff - 1).
14+
signaling BIP125 replaceability if any of its inputs have an nSequence number less than (0xffffffff - 1).
15+
A transaction also signals replaceibility if its nVersion field is set to 3.
1516

1617
*Rationale*: See [BIP125
1718
explanation](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki#motivation).

src/Makefile.am

+4
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ BITCOIN_CORE_H = \
242242
node/validation_cache_args.h \
243243
noui.h \
244244
outputtype.h \
245+
policy/v3_policy.h \
245246
policy/feerate.h \
246247
policy/fees.h \
247248
policy/fees_args.h \
@@ -441,6 +442,7 @@ libbitcoin_node_a_SOURCES = \
441442
node/utxo_snapshot.cpp \
442443
node/validation_cache_args.cpp \
443444
noui.cpp \
445+
policy/v3_policy.cpp \
444446
policy/fees.cpp \
445447
policy/fees_args.cpp \
446448
policy/packages.cpp \
@@ -702,6 +704,7 @@ libbitcoin_common_a_SOURCES = \
702704
netbase.cpp \
703705
net_permissions.cpp \
704706
outputtype.cpp \
707+
policy/v3_policy.cpp \
705708
policy/feerate.cpp \
706709
policy/policy.cpp \
707710
protocol.cpp \
@@ -960,6 +963,7 @@ libbitcoinkernel_la_SOURCES = \
960963
node/blockstorage.cpp \
961964
node/chainstate.cpp \
962965
node/utxo_snapshot.cpp \
966+
policy/v3_policy.cpp \
963967
policy/feerate.cpp \
964968
policy/packages.cpp \
965969
policy/policy.cpp \

src/policy/rbf.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx,
115115
}
116116

117117
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
118-
const std::set<uint256>& direct_conflicts,
118+
const std::set<Txid>& direct_conflicts,
119119
const uint256& txid)
120120
{
121121
for (CTxMemPool::txiter ancestorIt : ancestors) {
122-
const uint256& hashAncestor = ancestorIt->GetTx().GetHash();
122+
const Txid& hashAncestor = ancestorIt->GetTx().GetHash();
123123
if (direct_conflicts.count(hashAncestor)) {
124124
return strprintf("%s spends conflicting transaction %s",
125125
txid.ToString(),

src/policy/rbf.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx, const CTx
8080
* @returns error message if the sets intersect, std::nullopt if they are disjoint.
8181
*/
8282
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
83-
const std::set<uint256>& direct_conflicts,
83+
const std::set<Txid>& direct_conflicts,
8484
const uint256& txid);
8585

8686
/** Check that the feerate of the replacement transaction(s) is higher than the feerate of each

src/policy/v3_policy.cpp

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright (c) 2022 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <policy/v3_policy.h>
6+
7+
#include <coins.h>
8+
#include <consensus/amount.h>
9+
#include <logging.h>
10+
#include <tinyformat.h>
11+
#include <util/check.h>
12+
13+
#include <algorithm>
14+
#include <numeric>
15+
#include <vector>
16+
17+
/** Helper for PackageV3Checks: Returns a vector containing the indices of transactions (within
18+
* package) that are direct parents of ptx. */
19+
std::vector<size_t> FindInPackageParents(const Package& package, const CTransactionRef& ptx)
20+
{
21+
std::vector<size_t> in_package_parents;
22+
23+
std::set<Txid> possible_parents;
24+
for (auto &input : ptx->vin) {
25+
possible_parents.insert(input.prevout.hash);
26+
}
27+
28+
for (size_t i{0}; i < package.size(); ++i) {
29+
const auto& tx = package.at(i);
30+
// We assume the package is sorted, so that we don't need to continue
31+
// looking past the transaction itself.
32+
if (&(*tx) == &(*ptx)) break;
33+
if (possible_parents.count(tx->GetHash())) {
34+
in_package_parents.push_back(i);
35+
}
36+
}
37+
return in_package_parents;
38+
}
39+
40+
/** Helper for PackageV3Checks, storing info for a mempool or package parent. */
41+
struct ParentInfo {
42+
/** Txid used to identify this parent by prevout */
43+
const Txid& m_txid;
44+
/** Wtxid used for debug string */
45+
const Wtxid& m_wtxid;
46+
/** nVersion used to check inheritance of v3 and non-v3 */
47+
decltype(CTransaction::nVersion) m_version;
48+
/** If parent is in mempool, whether it has any descendants in mempool. */
49+
bool m_has_mempool_descendant;
50+
51+
ParentInfo() = delete;
52+
ParentInfo(const Txid& txid, const Wtxid& wtxid, decltype(CTransaction::nVersion) version, bool has_mempool_descendant) :
53+
m_txid{txid}, m_wtxid{wtxid}, m_version{version},
54+
m_has_mempool_descendant{has_mempool_descendant}
55+
{}
56+
};
57+
58+
std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t vsize,
59+
const Package& package,
60+
const CTxMemPool::setEntries& mempool_ancestors)
61+
{
62+
// This function is specialized for these limits, and must be reimplemented if they ever change.
63+
static_assert(V3_ANCESTOR_LIMIT == 2);
64+
static_assert(V3_DESCENDANT_LIMIT == 2);
65+
66+
const auto in_package_parents{FindInPackageParents(package, ptx)};
67+
68+
// Now we have all ancestors, so we can start checking v3 rules.
69+
if (ptx->nVersion == 3) {
70+
if (mempool_ancestors.size() + in_package_parents.size() + 1 > V3_ANCESTOR_LIMIT) {
71+
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
72+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString());
73+
}
74+
75+
const bool has_parent{mempool_ancestors.size() + in_package_parents.size() > 0};
76+
if (has_parent) {
77+
// A v3 child cannot be too large.
78+
if (vsize > V3_CHILD_MAX_VSIZE) {
79+
return strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
80+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
81+
vsize, V3_CHILD_MAX_VSIZE);
82+
}
83+
84+
const auto parent_info = [&] {
85+
if (mempool_ancestors.size() > 0) {
86+
// There's a parent in the mempool.
87+
auto& mempool_parent = *mempool_ancestors.begin();
88+
Assume(mempool_parent->GetCountWithDescendants() == 1);
89+
return ParentInfo{mempool_parent->GetTx().GetHash(),
90+
mempool_parent->GetTx().GetWitnessHash(),
91+
mempool_parent->GetTx().nVersion,
92+
/*has_mempool_descendant=*/mempool_parent->GetCountWithDescendants() > 1};
93+
} else {
94+
// Ancestor must be in the package. Find it.
95+
auto& parent_index = in_package_parents.front();
96+
auto& package_parent = package.at(parent_index);
97+
return ParentInfo{package_parent->GetHash(),
98+
package_parent->GetWitnessHash(),
99+
package_parent->nVersion,
100+
/*has_mempool_descendant=*/false};
101+
}
102+
}();
103+
104+
// If there is a parent, it must have the right version.
105+
if (parent_info.m_version != 3) {
106+
return strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
107+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
108+
parent_info.m_txid.ToString(), parent_info.m_wtxid.ToString());
109+
}
110+
111+
for (const auto& package_tx : package) {
112+
// Skip same tx.
113+
if (&(*package_tx) == &(*ptx)) continue;
114+
115+
for (auto& input : package_tx->vin) {
116+
// Fail if we find another tx with the same parent. We don't check whether the
117+
// sibling is to-be-replaced (done in SingleV3Checks) because these transactions
118+
// are within the same package.
119+
if (input.prevout.hash == parent_info.m_txid) {
120+
return strprintf("tx %s (wtxid=%s) would exceed descendant count limit",
121+
parent_info.m_txid.ToString(),
122+
parent_info.m_wtxid.ToString());
123+
}
124+
125+
// This tx can't have both a parent and an in-package child.
126+
if (input.prevout.hash == ptx->GetHash()) {
127+
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
128+
package_tx->GetHash().ToString(), package_tx->GetWitnessHash().ToString());
129+
}
130+
}
131+
}
132+
133+
// It shouldn't be possible to have any mempool siblings at this point. SingleV3Checks
134+
// catches mempool siblings. Also, if the package consists of connected transactions,
135+
// any tx having a mempool ancestor would mean the package exceeds ancestor limits.
136+
if (!Assume(!parent_info.m_has_mempool_descendant)) {
137+
return strprintf("tx %u would exceed descendant count limit", parent_info.m_wtxid.ToString());
138+
}
139+
}
140+
} else {
141+
// Non-v3 transactions cannot have v3 parents.
142+
for (auto it : mempool_ancestors) {
143+
if (it->GetTx().nVersion == 3) {
144+
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
145+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
146+
it->GetSharedTx()->GetHash().ToString(), it->GetSharedTx()->GetWitnessHash().ToString());
147+
}
148+
}
149+
for (const auto& index: in_package_parents) {
150+
if (package.at(index)->nVersion == 3) {
151+
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
152+
ptx->GetHash().ToString(),
153+
ptx->GetWitnessHash().ToString(),
154+
package.at(index)->GetHash().ToString(),
155+
package.at(index)->GetWitnessHash().ToString());
156+
}
157+
}
158+
}
159+
return std::nullopt;
160+
}
161+
162+
std::optional<std::string> SingleV3Checks(const CTransactionRef& ptx,
163+
const CTxMemPool::setEntries& mempool_ancestors,
164+
const std::set<Txid>& direct_conflicts,
165+
int64_t vsize)
166+
{
167+
// Check v3 and non-v3 inheritance.
168+
for (const auto& entry : mempool_ancestors) {
169+
if (ptx->nVersion != 3 && entry->GetTx().nVersion == 3) {
170+
return strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)",
171+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
172+
entry->GetSharedTx()->GetHash().ToString(), entry->GetSharedTx()->GetWitnessHash().ToString());
173+
} else if (ptx->nVersion == 3 && entry->GetTx().nVersion != 3) {
174+
return strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)",
175+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(),
176+
entry->GetSharedTx()->GetHash().ToString(), entry->GetSharedTx()->GetWitnessHash().ToString());
177+
}
178+
}
179+
180+
// This function is specialized for these limits, and must be reimplemented if they ever change.
181+
static_assert(V3_ANCESTOR_LIMIT == 2);
182+
static_assert(V3_DESCENDANT_LIMIT == 2);
183+
184+
// The rest of the rules only apply to transactions with nVersion=3.
185+
if (ptx->nVersion != 3) return std::nullopt;
186+
187+
// Check that V3_ANCESTOR_LIMIT would not be violated, including both in-package and in-mempool.
188+
if (mempool_ancestors.size() + 1 > V3_ANCESTOR_LIMIT) {
189+
return strprintf("tx %s (wtxid=%s) would have too many ancestors",
190+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString());
191+
}
192+
193+
// Remaining checks only pertain to transactions with unconfirmed ancestors.
194+
if (mempool_ancestors.size() > 0) {
195+
// If this transaction spends V3 parents, it cannot be too large.
196+
if (vsize > V3_CHILD_MAX_VSIZE) {
197+
return strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes",
198+
ptx->GetHash().ToString(), ptx->GetWitnessHash().ToString(), vsize, V3_CHILD_MAX_VSIZE);
199+
}
200+
201+
// Check the descendant counts of in-mempool ancestors.
202+
const auto& parent_entry = *mempool_ancestors.begin();
203+
// If there are any ancestors, this is the only child allowed. The parent cannot have any
204+
// other descendants. We handle the possibility of multiple children as that case is
205+
// possible through a reorg.
206+
const auto& children = parent_entry->GetMemPoolChildrenConst();
207+
// Don't double-count a transaction that is going to be replaced. This logic assumes that
208+
// any descendant of the V3 transaction is a direct child, which makes sense because a V3
209+
// transaction can only have 1 descendant.
210+
const bool child_will_be_replaced = !children.empty() &&
211+
std::any_of(children.cbegin(), children.cend(),
212+
[&direct_conflicts](const CTxMemPoolEntry& child){return direct_conflicts.count(child.GetTx().GetHash()) > 0;});
213+
if (parent_entry->GetCountWithDescendants() + 1 > V3_DESCENDANT_LIMIT && !child_will_be_replaced) {
214+
return strprintf("tx %u (wtxid=%s) would exceed descendant count limit",
215+
parent_entry->GetSharedTx()->GetHash().ToString(),
216+
parent_entry->GetSharedTx()->GetWitnessHash().ToString());
217+
}
218+
}
219+
return std::nullopt;
220+
}

src/policy/v3_policy.h

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2022 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_POLICY_V3_POLICY_H
6+
#define BITCOIN_POLICY_V3_POLICY_H
7+
8+
#include <consensus/amount.h>
9+
#include <policy/packages.h>
10+
#include <policy/policy.h>
11+
#include <primitives/transaction.h>
12+
#include <txmempool.h>
13+
#include <util/result.h>
14+
15+
#include <set>
16+
#include <string>
17+
18+
// This module enforces rules for transactions with nVersion=3 ("v3 transactions") which help make
19+
// RBF abilities more robust.
20+
21+
// v3 only allows 1 parent and 1 child when unconfirmed.
22+
/** Maximum number of transactions including an unconfirmed tx and its descendants. */
23+
static constexpr unsigned int V3_DESCENDANT_LIMIT{2};
24+
/** Maximum number of transactions including a V3 tx and all its mempool ancestors. */
25+
static constexpr unsigned int V3_ANCESTOR_LIMIT{2};
26+
27+
/** Maximum sigop-adjusted virtual size of a tx which spends from an unconfirmed v3 transaction. */
28+
static constexpr int64_t V3_CHILD_MAX_VSIZE{1000};
29+
// These limits are within the default ancestor/descendant limits.
30+
static_assert(V3_CHILD_MAX_VSIZE + MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR <= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000);
31+
static_assert(V3_CHILD_MAX_VSIZE + MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR <= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * 1000);
32+
33+
/** Must be called for every transaction, even if not v3. Not strictly necessary for transactions
34+
* accepted through AcceptMultipleTransactions.
35+
*
36+
* Checks the following rules:
37+
* 1. A v3 tx must only have v3 unconfirmed ancestors.
38+
* 2. A non-v3 tx must only have non-v3 unconfirmed ancestors.
39+
* 3. A v3's ancestor set, including itself, must be within V3_ANCESTOR_LIMIT.
40+
* 4. A v3's descendant set, including itself, must be within V3_DESCENDANT_LIMIT.
41+
* 5. If a v3 tx has any unconfirmed ancestors, the tx's sigop-adjusted vsize must be within
42+
* V3_CHILD_MAX_VSIZE.
43+
*
44+
*
45+
* @param[in] mempool_ancestors The in-mempool ancestors of ptx.
46+
* @param[in] direct_conflicts In-mempool transactions this tx conflicts with. These conflicts
47+
* are used to more accurately calculate the resulting descendant
48+
* count of in-mempool ancestors.
49+
* @param[in] vsize The sigop-adjusted virtual size of ptx.
50+
*
51+
* @returns debug string if an error occurs, std::nullopt otherwise.
52+
*/
53+
std::optional<std::string> SingleV3Checks(const CTransactionRef& ptx,
54+
const CTxMemPool::setEntries& mempool_ancestors,
55+
const std::set<Txid>& direct_conflicts,
56+
int64_t vsize);
57+
58+
/** Must be called for every transaction that is submitted within a package, even if not v3.
59+
*
60+
* For each transaction in a package:
61+
* If it's not a v3 transaction, verify it has no direct v3 parents in the mempool or the package.
62+
63+
* If it is a v3 transaction, verify that any direct parents in the mempool or the package are v3.
64+
* If such a parent exists, verify that parent has no other children in the package or the mempool,
65+
* and that the transaction itself has no children in the package.
66+
*
67+
* If any v3 violations in the package exist, this test will fail for one of them:
68+
* - if a v3 transaction T has a parent in the mempool and a child in the package, then PV3C(T) will fail
69+
* - if a v3 transaction T has a parent in the package and a child in the package, then PV3C(T) will fail
70+
* - if a v3 transaction T and a v3 (sibling) transaction U have some parent in the mempool,
71+
* then PV3C(T) and PV3C(U) will fail
72+
* - if a v3 transaction T and a v3 (sibling) transaction U have some parent in the package,
73+
* then PV3C(T) and PV3C(U) will fail
74+
* - if a v3 transaction T has a parent P and a grandparent G in the package, then
75+
* PV3C(P) will fail (though PV3C(G) and PV3C(T) might succeed).
76+
*
77+
* @returns debug string if an error occurs, std::nullopt otherwise.
78+
* */
79+
std::optional<std::string> PackageV3Checks(const CTransactionRef& ptx, int64_t vsize,
80+
const Package& package,
81+
const CTxMemPool::setEntries& mempool_ancestors);
82+
83+
#endif // BITCOIN_POLICY_V3_POLICY_H

src/rpc/mempool.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ static RPCHelpMan testmempoolaccept()
199199
result_inner.pushKV("txid", tx->GetHash().GetHex());
200200
result_inner.pushKV("wtxid", tx->GetWitnessHash().GetHex());
201201
if (package_result.m_state.GetResult() == PackageValidationResult::PCKG_POLICY) {
202-
result_inner.pushKV("package-error", package_result.m_state.GetRejectReason());
202+
result_inner.pushKV("package-error", package_result.m_state.ToString());
203203
}
204204
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
205205
if (exit_early || it == package_result.m_tx_results.end()) {
@@ -907,7 +907,7 @@ static RPCHelpMan submitpackage()
907907
case PackageValidationResult::PCKG_TX:
908908
{
909909
// Package-wide error we want to return, but we also want to return individual responses
910-
package_msg = package_result.m_state.GetRejectReason();
910+
package_msg = package_result.m_state.ToString();
911911
CHECK_NONFATAL(package_result.m_tx_results.size() == txns.size() ||
912912
package_result.m_tx_results.empty());
913913
break;

0 commit comments

Comments
 (0)