|
| 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