Skip to content

Commit 82bc7fa

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#21946: Document and test lack of inherited signaling in RBF policy
2eb0eed validation: document lack of inherited signaling in RBF policy (Antoine Riard) 906b6d9 test: Extend feature_rbf.py with no inherited signaling (Antoine Riard) Pull request description: Contrary to BIP125 or other full-node implementation (e.g btcd), Bitcoin Core's mempool policy doesn't implement inherited signaling. This PR documents our mempool behavior on this and add a test demonstrating the case. ACKs for top commit: jonatack: ACK 2eb0eed benthecarman: ACK 2eb0eed Tree-SHA512: d41453d3b49bae3c1eb532a968f43bc047084913bd285929d4d9cba142777ff2be38163d912e28dfc635f4ecf446de68effad799c6e71be52f81e83410c712fb
2 parents 45a8b01 + 2eb0eed commit 82bc7fa

File tree

2 files changed

+74
-4
lines changed

2 files changed

+74
-4
lines changed

src/validation.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
629629
// is for the sake of multi-party protocols, where we don't
630630
// want a single party to be able to disable replacement.
631631
//
632-
// The opt-out ignores descendants as anyone relying on
633-
// first-seen mempool behavior should be checking all
634-
// unconfirmed ancestors anyway; doing otherwise is hopelessly
635-
// insecure.
632+
// Transactions that don't explicitly signal replaceability are
633+
// *not* replaceable with the current logic, even if one of their
634+
// unconfirmed ancestors signals replaceability. This diverges
635+
// from BIP125's inherited signaling description (see CVE-2021-31876).
636+
// Applications relying on first-seen mempool behavior should
637+
// check all unconfirmed ancestors; otherwise an opt-in ancestor
638+
// might be replaced, causing removal of this descendant.
636639
bool fReplacementOptOut = true;
637640
for (const CTxIn &_txin : ptxConflicting->vin)
638641
{

test/functional/feature_rbf.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def run_test(self):
116116
self.log.info("Running test prioritised transactions...")
117117
self.test_prioritised_transactions()
118118

119+
self.log.info("Running test no inherited signaling...")
120+
self.test_no_inherited_signaling()
121+
119122
self.log.info("Passed")
120123

121124
def test_simple_doublespend(self):
@@ -564,5 +567,69 @@ def test_rpc(self):
564567
assert_equal(json0["vin"][0]["sequence"], 4294967293)
565568
assert_equal(json1["vin"][0]["sequence"], 4294967294)
566569

570+
def test_no_inherited_signaling(self):
571+
# Send tx from which to conflict outputs later
572+
base_txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
573+
self.nodes[0].generate(1)
574+
self.sync_blocks()
575+
576+
# Create an explicitly opt-in parent transaction
577+
optin_parent_tx = self.nodes[0].createrawtransaction([{
578+
'txid': base_txid,
579+
'vout': 0,
580+
"sequence": 0xfffffffd,
581+
}], {self.nodes[0].getnewaddress(): Decimal("9.99998")})
582+
583+
optin_parent_tx = self.nodes[0].signrawtransactionwithwallet(optin_parent_tx)
584+
585+
# Broadcast parent tx
586+
optin_parent_txid = self.nodes[0].sendrawtransaction(hexstring=optin_parent_tx["hex"], maxfeerate=0)
587+
assert optin_parent_txid in self.nodes[0].getrawmempool()
588+
589+
replacement_parent_tx = self.nodes[0].createrawtransaction([{
590+
'txid': base_txid,
591+
'vout': 0,
592+
"sequence": 0xfffffffd,
593+
}], {self.nodes[0].getnewaddress(): Decimal("9.90000")})
594+
595+
replacement_parent_tx = self.nodes[0].signrawtransactionwithwallet(replacement_parent_tx)
596+
597+
# Test if parent tx can be replaced.
598+
res = self.nodes[0].testmempoolaccept(rawtxs=[replacement_parent_tx['hex']], maxfeerate=0)[0]
599+
600+
# Parent can be replaced.
601+
assert_equal(res['allowed'], True)
602+
603+
# Create an opt-out child tx spending the opt-in parent
604+
optout_child_tx = self.nodes[0].createrawtransaction([{
605+
'txid': optin_parent_txid,
606+
'vout': 0,
607+
"sequence": 0xffffffff,
608+
}], {self.nodes[0].getnewaddress(): Decimal("9.99990")})
609+
610+
optout_child_tx = self.nodes[0].signrawtransactionwithwallet(optout_child_tx)
611+
612+
# Broadcast child tx
613+
optout_child_txid = self.nodes[0].sendrawtransaction(hexstring=optout_child_tx["hex"], maxfeerate=0)
614+
assert optout_child_txid in self.nodes[0].getrawmempool()
615+
616+
replacement_child_tx = self.nodes[0].createrawtransaction([{
617+
'txid': optin_parent_txid,
618+
'vout': 0,
619+
"sequence": 0xffffffff,
620+
}], {self.nodes[0].getnewaddress(): Decimal("9.00000")})
621+
622+
replacement_child_tx = self.nodes[0].signrawtransactionwithwallet(replacement_child_tx)
623+
624+
# Broadcast replacement child tx
625+
# BIP 125 :
626+
# 1. The original transactions signal replaceability explicitly or through inheritance as described in the above
627+
# Summary section.
628+
# The original transaction (`optout_child_tx`) doesn't signal RBF but its parent (`optin_parent_txid`) does.
629+
# The replacement transaction (`replacement_child_tx`) should be able to replace the original transaction.
630+
# See CVE-2021-31876 for further explanations.
631+
assert optin_parent_txid in self.nodes[0].getrawmempool()
632+
assert_raises_rpc_error(-26, 'txn-mempool-conflict', self.nodes[0].sendrawtransaction, replacement_child_tx["hex"], 0)
633+
567634
if __name__ == '__main__':
568635
ReplaceByFeeTest().main()

0 commit comments

Comments
 (0)