Skip to content

Commit 0e93586

Browse files
committed
Merge pull request #6871
63b5840 Fix usage of local python-bitcoinlib (Peter Todd) 16a2f93 Fix incorrect locking of mempool during RBF replacement (Peter Todd) 97203f5 Port test to rpc-test framework (Suhas Daftuar) 20367d8 Add test for max replacement limit (Suhas Daftuar) 73d9040 Improve RBF replacement criteria (Suhas Daftuar) b272ecf Reject replacements that add new unconfirmed inputs (Peter Todd) fc8c19a Prevent low feerate txs from (directly) replacing high feerate txs (Peter Todd) 0137e6f Add tests for transaction replacement (Peter Todd) 5891f87 Add opt-in full-RBF to mempool (Peter Todd)
2 parents e92377f + 63b5840 commit 0e93586

File tree

7 files changed

+1089
-9
lines changed

7 files changed

+1089
-9
lines changed

qa/pull-tester/rpc-tests.py

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
'p2p-acceptblock.py',
113113
'mempool_packages.py',
114114
'maxuploadtarget.py',
115+
'replace-by-fee.py',
115116
]
116117

117118
#Enable ZMQ tests

qa/replace-by-fee/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-bitcoinlib

qa/replace-by-fee/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Replace-by-fee regression tests
2+
===============================
3+
4+
First get version v0.5.0 of the python-bitcoinlib library. In this directory
5+
run:
6+
7+
git clone -n https://github.com/petertodd/python-bitcoinlib
8+
(cd python-bitcoinlib && git checkout 8270bfd9c6ac37907d75db3d8b9152d61c7255cd)
9+
10+
Then run the tests themselves with a bitcoind available running in regtest
11+
mode:
12+
13+
./rbf-tests.py

qa/replace-by-fee/rbf-tests.py

+360
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2015 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
#
7+
# Test replace-by-fee
8+
#
9+
10+
import os
11+
import sys
12+
13+
# Add python-bitcoinlib to module search path, prior to any system-wide
14+
# python-bitcoinlib.
15+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "python-bitcoinlib"))
16+
17+
import unittest
18+
19+
import bitcoin
20+
bitcoin.SelectParams('regtest')
21+
22+
import bitcoin.rpc
23+
24+
from bitcoin.core import *
25+
from bitcoin.core.script import *
26+
from bitcoin.wallet import *
27+
28+
MAX_REPLACEMENT_LIMIT = 100
29+
30+
class Test_ReplaceByFee(unittest.TestCase):
31+
proxy = None
32+
33+
@classmethod
34+
def setUpClass(cls):
35+
if cls.proxy is None:
36+
cls.proxy = bitcoin.rpc.Proxy()
37+
38+
@classmethod
39+
def mine_mempool(cls):
40+
"""Mine until mempool is empty"""
41+
mempool_size = 1
42+
while mempool_size:
43+
cls.proxy.call('generate', 1)
44+
new_mempool_size = len(cls.proxy.getrawmempool())
45+
46+
# It's possible to get stuck in a loop here if the mempool has
47+
# transactions that can't be mined.
48+
assert(new_mempool_size != mempool_size)
49+
mempool_size = new_mempool_size
50+
51+
@classmethod
52+
def tearDownClass(cls):
53+
# Make sure mining works
54+
cls.mine_mempool()
55+
56+
def make_txout(self, amount, confirmed=True, scriptPubKey=CScript([1])):
57+
"""Create a txout with a given amount and scriptPubKey
58+
59+
Mines coins as needed.
60+
61+
confirmed - txouts created will be confirmed in the blockchain;
62+
unconfirmed otherwise.
63+
"""
64+
fee = 1*COIN
65+
while self.proxy.getbalance() < amount + fee:
66+
self.proxy.call('generate', 100)
67+
68+
addr = P2SHBitcoinAddress.from_redeemScript(CScript([]))
69+
txid = self.proxy.sendtoaddress(addr, amount + fee)
70+
71+
tx1 = self.proxy.getrawtransaction(txid)
72+
73+
i = None
74+
for i, txout in enumerate(tx1.vout):
75+
if txout.scriptPubKey == addr.to_scriptPubKey():
76+
break
77+
assert i is not None
78+
79+
tx2 = CTransaction([CTxIn(COutPoint(txid, i), CScript([1, CScript([])]), nSequence=0)],
80+
[CTxOut(amount, scriptPubKey)])
81+
82+
tx2_txid = self.proxy.sendrawtransaction(tx2, True)
83+
84+
# If requested, ensure txouts are confirmed.
85+
if confirmed:
86+
self.mine_mempool()
87+
88+
return COutPoint(tx2_txid, 0)
89+
90+
def test_simple_doublespend(self):
91+
"""Simple doublespend"""
92+
tx0_outpoint = self.make_txout(1.1*COIN)
93+
94+
tx1a = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
95+
[CTxOut(1*COIN, CScript([b'a']))])
96+
tx1a_txid = self.proxy.sendrawtransaction(tx1a, True)
97+
98+
# Should fail because we haven't changed the fee
99+
tx1b = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
100+
[CTxOut(1*COIN, CScript([b'b']))])
101+
102+
try:
103+
tx1b_txid = self.proxy.sendrawtransaction(tx1b, True)
104+
except bitcoin.rpc.JSONRPCException as exp:
105+
self.assertEqual(exp.error['code'], -26) # insufficient fee
106+
else:
107+
self.fail()
108+
109+
# Extra 0.1 BTC fee
110+
tx1b = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
111+
[CTxOut(0.9*COIN, CScript([b'b']))])
112+
tx1b_txid = self.proxy.sendrawtransaction(tx1b, True)
113+
114+
# tx1a is in fact replaced
115+
with self.assertRaises(IndexError):
116+
self.proxy.getrawtransaction(tx1a_txid)
117+
118+
self.assertEqual(tx1b, self.proxy.getrawtransaction(tx1b_txid))
119+
120+
def test_doublespend_chain(self):
121+
"""Doublespend of a long chain"""
122+
123+
initial_nValue = 50*COIN
124+
tx0_outpoint = self.make_txout(initial_nValue)
125+
126+
prevout = tx0_outpoint
127+
remaining_value = initial_nValue
128+
chain_txids = []
129+
while remaining_value > 10*COIN:
130+
remaining_value -= 1*COIN
131+
tx = CTransaction([CTxIn(prevout, nSequence=0)],
132+
[CTxOut(remaining_value, CScript([1]))])
133+
txid = self.proxy.sendrawtransaction(tx, True)
134+
chain_txids.append(txid)
135+
prevout = COutPoint(txid, 0)
136+
137+
# Whether the double-spend is allowed is evaluated by including all
138+
# child fees - 40 BTC - so this attempt is rejected.
139+
dbl_tx = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
140+
[CTxOut(initial_nValue - 30*COIN, CScript([1]))])
141+
142+
try:
143+
self.proxy.sendrawtransaction(dbl_tx, True)
144+
except bitcoin.rpc.JSONRPCException as exp:
145+
self.assertEqual(exp.error['code'], -26) # insufficient fee
146+
else:
147+
self.fail()
148+
149+
# Accepted with sufficient fee
150+
dbl_tx = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
151+
[CTxOut(1*COIN, CScript([1]))])
152+
self.proxy.sendrawtransaction(dbl_tx, True)
153+
154+
for doublespent_txid in chain_txids:
155+
with self.assertRaises(IndexError):
156+
self.proxy.getrawtransaction(doublespent_txid)
157+
158+
def test_doublespend_tree(self):
159+
"""Doublespend of a big tree of transactions"""
160+
161+
initial_nValue = 50*COIN
162+
tx0_outpoint = self.make_txout(initial_nValue)
163+
164+
def branch(prevout, initial_value, max_txs, *, tree_width=5, fee=0.0001*COIN, _total_txs=None):
165+
if _total_txs is None:
166+
_total_txs = [0]
167+
if _total_txs[0] >= max_txs:
168+
return
169+
170+
txout_value = (initial_value - fee) // tree_width
171+
if txout_value < fee:
172+
return
173+
174+
vout = [CTxOut(txout_value, CScript([i+1]))
175+
for i in range(tree_width)]
176+
tx = CTransaction([CTxIn(prevout, nSequence=0)],
177+
vout)
178+
179+
self.assertTrue(len(tx.serialize()) < 100000)
180+
txid = self.proxy.sendrawtransaction(tx, True)
181+
yield tx
182+
_total_txs[0] += 1
183+
184+
for i, txout in enumerate(tx.vout):
185+
yield from branch(COutPoint(txid, i), txout_value,
186+
max_txs,
187+
tree_width=tree_width, fee=fee,
188+
_total_txs=_total_txs)
189+
190+
fee = 0.0001*COIN
191+
n = MAX_REPLACEMENT_LIMIT
192+
tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee))
193+
self.assertEqual(len(tree_txs), n)
194+
195+
# Attempt double-spend, will fail because too little fee paid
196+
dbl_tx = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
197+
[CTxOut(initial_nValue - fee*n, CScript([1]))])
198+
try:
199+
self.proxy.sendrawtransaction(dbl_tx, True)
200+
except bitcoin.rpc.JSONRPCException as exp:
201+
self.assertEqual(exp.error['code'], -26) # insufficient fee
202+
else:
203+
self.fail()
204+
205+
# 1 BTC fee is enough
206+
dbl_tx = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
207+
[CTxOut(initial_nValue - fee*n - 1*COIN, CScript([1]))])
208+
self.proxy.sendrawtransaction(dbl_tx, True)
209+
210+
for tx in tree_txs:
211+
with self.assertRaises(IndexError):
212+
self.proxy.getrawtransaction(tx.GetHash())
213+
214+
# Try again, but with more total transactions than the "max txs
215+
# double-spent at once" anti-DoS limit.
216+
for n in (MAX_REPLACEMENT_LIMIT, MAX_REPLACEMENT_LIMIT*2):
217+
fee = 0.0001*COIN
218+
tx0_outpoint = self.make_txout(initial_nValue)
219+
tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee))
220+
self.assertEqual(len(tree_txs), n)
221+
222+
dbl_tx = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
223+
[CTxOut(initial_nValue - fee*n, CScript([1]))])
224+
try:
225+
self.proxy.sendrawtransaction(dbl_tx, True)
226+
except bitcoin.rpc.JSONRPCException as exp:
227+
self.assertEqual(exp.error['code'], -26)
228+
else:
229+
self.fail()
230+
231+
for tx in tree_txs:
232+
self.proxy.getrawtransaction(tx.GetHash())
233+
234+
def test_replacement_feeperkb(self):
235+
"""Replacement requires fee-per-KB to be higher"""
236+
tx0_outpoint = self.make_txout(1.1*COIN)
237+
238+
tx1a = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
239+
[CTxOut(1*COIN, CScript([b'a']))])
240+
tx1a_txid = self.proxy.sendrawtransaction(tx1a, True)
241+
242+
# Higher fee, but the fee per KB is much lower, so the replacement is
243+
# rejected.
244+
tx1b = CTransaction([CTxIn(tx0_outpoint, nSequence=0)],
245+
[CTxOut(0.001*COIN,
246+
CScript([b'a'*999000]))])
247+
248+
try:
249+
tx1b_txid = self.proxy.sendrawtransaction(tx1b, True)
250+
except bitcoin.rpc.JSONRPCException as exp:
251+
self.assertEqual(exp.error['code'], -26) # insufficient fee
252+
else:
253+
self.fail()
254+
255+
def test_spends_of_conflicting_outputs(self):
256+
"""Replacements that spend conflicting tx outputs are rejected"""
257+
utxo1 = self.make_txout(1.2*COIN)
258+
utxo2 = self.make_txout(3.0*COIN)
259+
260+
tx1a = CTransaction([CTxIn(utxo1, nSequence=0)],
261+
[CTxOut(1.1*COIN, CScript([b'a']))])
262+
tx1a_txid = self.proxy.sendrawtransaction(tx1a, True)
263+
264+
# Direct spend an output of the transaction we're replacing.
265+
tx2 = CTransaction([CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0),
266+
CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)],
267+
tx1a.vout)
268+
269+
try:
270+
tx2_txid = self.proxy.sendrawtransaction(tx2, True)
271+
except bitcoin.rpc.JSONRPCException as exp:
272+
self.assertEqual(exp.error['code'], -26)
273+
else:
274+
self.fail()
275+
276+
# Spend tx1a's output to test the indirect case.
277+
tx1b = CTransaction([CTxIn(COutPoint(tx1a_txid, 0), nSequence=0)],
278+
[CTxOut(1.0*COIN, CScript([b'a']))])
279+
tx1b_txid = self.proxy.sendrawtransaction(tx1b, True)
280+
281+
tx2 = CTransaction([CTxIn(utxo1, nSequence=0), CTxIn(utxo2, nSequence=0),
282+
CTxIn(COutPoint(tx1b_txid, 0))],
283+
tx1a.vout)
284+
285+
try:
286+
tx2_txid = self.proxy.sendrawtransaction(tx2, True)
287+
except bitcoin.rpc.JSONRPCException as exp:
288+
self.assertEqual(exp.error['code'], -26)
289+
else:
290+
self.fail()
291+
292+
def test_new_unconfirmed_inputs(self):
293+
"""Replacements that add new unconfirmed inputs are rejected"""
294+
confirmed_utxo = self.make_txout(1.1*COIN)
295+
unconfirmed_utxo = self.make_txout(0.1*COIN, False)
296+
297+
tx1 = CTransaction([CTxIn(confirmed_utxo)],
298+
[CTxOut(1.0*COIN, CScript([b'a']))])
299+
tx1_txid = self.proxy.sendrawtransaction(tx1, True)
300+
301+
tx2 = CTransaction([CTxIn(confirmed_utxo), CTxIn(unconfirmed_utxo)],
302+
tx1.vout)
303+
304+
try:
305+
tx2_txid = self.proxy.sendrawtransaction(tx2, True)
306+
except bitcoin.rpc.JSONRPCException as exp:
307+
self.assertEqual(exp.error['code'], -26)
308+
else:
309+
self.fail()
310+
311+
def test_too_many_replacements(self):
312+
"""Replacements that evict too many transactions are rejected"""
313+
# Try directly replacing more than MAX_REPLACEMENT_LIMIT
314+
# transactions
315+
316+
# Start by creating a single transaction with many outputs
317+
initial_nValue = 10*COIN
318+
utxo = self.make_txout(initial_nValue)
319+
fee = 0.0001*COIN
320+
split_value = int((initial_nValue-fee)/(MAX_REPLACEMENT_LIMIT+1))
321+
actual_fee = initial_nValue - split_value*(MAX_REPLACEMENT_LIMIT+1)
322+
323+
outputs = []
324+
for i in range(MAX_REPLACEMENT_LIMIT+1):
325+
outputs.append(CTxOut(split_value, CScript([1])))
326+
327+
splitting_tx = CTransaction([CTxIn(utxo, nSequence=0)], outputs)
328+
txid = self.proxy.sendrawtransaction(splitting_tx, True)
329+
330+
# Now spend each of those outputs individually
331+
for i in range(MAX_REPLACEMENT_LIMIT+1):
332+
tx_i = CTransaction([CTxIn(COutPoint(txid, i), nSequence=0)],
333+
[CTxOut(split_value-fee, CScript([b'a']))])
334+
self.proxy.sendrawtransaction(tx_i, True)
335+
336+
# Now create doublespend of the whole lot, should fail
337+
# Need a big enough fee to cover all spending transactions and have
338+
# a higher fee rate
339+
double_spend_value = (split_value-100*fee)*(MAX_REPLACEMENT_LIMIT+1)
340+
inputs = []
341+
for i in range(MAX_REPLACEMENT_LIMIT+1):
342+
inputs.append(CTxIn(COutPoint(txid, i), nSequence=0))
343+
double_tx = CTransaction(inputs, [CTxOut(double_spend_value, CScript([b'a']))])
344+
345+
try:
346+
self.proxy.sendrawtransaction(double_tx, True)
347+
except bitcoin.rpc.JSONRPCException as exp:
348+
self.assertEqual(exp.error['code'], -26)
349+
self.assertEqual("too many potential replacements" in exp.error['message'], True)
350+
else:
351+
self.fail()
352+
353+
# If we remove an input, it should pass
354+
double_tx = CTransaction(inputs[0:-1],
355+
[CTxOut(double_spend_value, CScript([b'a']))])
356+
357+
self.proxy.sendrawtransaction(double_tx, True)
358+
359+
if __name__ == '__main__':
360+
unittest.main()

0 commit comments

Comments
 (0)