/ test / functional / mempool_accept.py
mempool_accept.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2017-2022 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  """Test mempool acceptance of raw transactions."""
  6  
  7  from copy import deepcopy
  8  from decimal import Decimal
  9  import math
 10  
 11  from test_framework.test_framework import BitcoinTestFramework
 12  from test_framework.messages import (
 13      MAX_BIP125_RBF_SEQUENCE,
 14      COIN,
 15      COutPoint,
 16      CTransaction,
 17      CTxIn,
 18      CTxInWitness,
 19      CTxOut,
 20      MAX_BLOCK_WEIGHT,
 21      MAX_MONEY,
 22      SEQUENCE_FINAL,
 23      tx_from_hex,
 24  )
 25  from test_framework.script import (
 26      CScript,
 27      OP_0,
 28      OP_HASH160,
 29      OP_RETURN,
 30      OP_TRUE,
 31  )
 32  from test_framework.script_util import (
 33      DUMMY_MIN_OP_RETURN_SCRIPT,
 34      keys_to_multisig_script,
 35      MIN_PADDING,
 36      MIN_STANDARD_TX_NONWITNESS_SIZE,
 37      script_to_p2sh_script,
 38      script_to_p2wsh_script,
 39  )
 40  from test_framework.util import (
 41      assert_equal,
 42      assert_greater_than,
 43      assert_raises_rpc_error,
 44  )
 45  from test_framework.wallet import MiniWallet
 46  from test_framework.wallet_util import generate_keypair
 47  
 48  
 49  class MempoolAcceptanceTest(BitcoinTestFramework):
 50      def set_test_params(self):
 51          self.num_nodes = 1
 52          self.extra_args = [[
 53              '-txindex','-permitbaremultisig=0',
 54          ]] * self.num_nodes
 55          self.supports_cli = False
 56  
 57      def check_mempool_result(self, result_expected, *args, **kwargs):
 58          """Wrapper to check result of testmempoolaccept on node_0's mempool"""
 59          result_test = self.nodes[0].testmempoolaccept(*args, **kwargs)
 60          for r in result_test:
 61              # Skip these checks for now
 62              r.pop('wtxid')
 63              if "fees" in r:
 64                  r["fees"].pop("effective-feerate")
 65                  r["fees"].pop("effective-includes")
 66          assert_equal(result_expected, result_test)
 67          assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size)  # Must not change mempool state
 68  
 69      def run_test(self):
 70          node = self.nodes[0]
 71          self.wallet = MiniWallet(node)
 72  
 73          self.log.info('Start with empty mempool, and 200 blocks')
 74          self.mempool_size = 0
 75          assert_equal(node.getblockcount(), 200)
 76          assert_equal(node.getmempoolinfo()['size'], self.mempool_size)
 77  
 78          self.log.info('Should not accept garbage to testmempoolaccept')
 79          assert_raises_rpc_error(-3, 'JSON value of type string is not of expected type array', lambda: node.testmempoolaccept(rawtxs='ff00baar'))
 80          assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=['ff22']*26))
 81          assert_raises_rpc_error(-8, 'Array must contain between 1 and 25 transactions.', lambda: node.testmempoolaccept(rawtxs=[]))
 82          assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar']))
 83  
 84          self.log.info('A transaction already in the blockchain')
 85          tx = self.wallet.create_self_transfer()['tx']  # Pick a random coin(base) to spend
 86          tx.vout.append(deepcopy(tx.vout[0]))
 87          tx.vout[0].nValue = int(0.3 * COIN)
 88          tx.vout[1].nValue = int(49 * COIN)
 89          raw_tx_in_block = tx.serialize().hex()
 90          txid_in_block = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_in_block)
 91          self.generate(node, 1)
 92          self.mempool_size = 0
 93          # Also check feerate. 1BTC/kvB fails
 94          assert_raises_rpc_error(-8, "Fee rates larger than or equal to 1BTC/kvB are not accepted", lambda: self.check_mempool_result(
 95              result_expected=None,
 96              rawtxs=[raw_tx_in_block],
 97              maxfeerate=1,
 98          ))
 99          # Check negative feerate
100          assert_raises_rpc_error(-3, "Amount out of range", lambda: self.check_mempool_result(
101              result_expected=None,
102              rawtxs=[raw_tx_in_block],
103              maxfeerate=-0.01,
104          ))
105          # ... 0.99 passes
106          self.check_mempool_result(
107              result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': 'txn-already-known'}],
108              rawtxs=[raw_tx_in_block],
109              maxfeerate=0.99,
110          )
111  
112          self.log.info('A transaction not in the mempool')
113          fee = Decimal('0.000007')
114          utxo_to_spend = self.wallet.get_utxo(txid=txid_in_block)  # use 0.3 BTC UTXO
115          tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=MAX_BIP125_RBF_SEQUENCE)['tx']
116          tx.vout[0].nValue = int((Decimal('0.3') - fee) * COIN)
117          raw_tx_0 = tx.serialize().hex()
118          txid_0 = tx.rehash()
119          self.check_mempool_result(
120              result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee}}],
121              rawtxs=[raw_tx_0],
122          )
123  
124          self.log.info('A final transaction not in the mempool')
125          output_amount = Decimal('0.025')
126          tx = self.wallet.create_self_transfer(
127              sequence=SEQUENCE_FINAL,
128              locktime=node.getblockcount() + 2000,  # Can be anything
129          )['tx']
130          tx.vout[0].nValue = int(output_amount * COIN)
131          raw_tx_final = tx.serialize().hex()
132          tx = tx_from_hex(raw_tx_final)
133          fee_expected = Decimal('50.0') - output_amount
134          self.check_mempool_result(
135              result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': fee_expected}}],
136              rawtxs=[tx.serialize().hex()],
137              maxfeerate=0,
138          )
139          node.sendrawtransaction(hexstring=raw_tx_final, maxfeerate=0)
140          self.mempool_size += 1
141  
142          self.log.info('A transaction in the mempool')
143          node.sendrawtransaction(hexstring=raw_tx_0)
144          self.mempool_size += 1
145          self.check_mempool_result(
146              result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'txn-already-in-mempool'}],
147              rawtxs=[raw_tx_0],
148          )
149  
150          self.log.info('A transaction that replaces a mempool transaction')
151          tx = tx_from_hex(raw_tx_0)
152          tx.vout[0].nValue -= int(fee * COIN)  # Double the fee
153          tx.vin[0].nSequence = MAX_BIP125_RBF_SEQUENCE + 1  # Now, opt out of RBF
154          raw_tx_0 = tx.serialize().hex()
155          txid_0 = tx.rehash()
156          self.check_mempool_result(
157              result_expected=[{'txid': txid_0, 'allowed': True, 'vsize': tx.get_vsize(), 'fees': {'base': (2 * fee)}}],
158              rawtxs=[raw_tx_0],
159          )
160  
161          self.log.info('A transaction that conflicts with an unconfirmed tx')
162          # Send the transaction that replaces the mempool transaction and opts out of replaceability
163          node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0)
164          # take original raw_tx_0
165          tx = tx_from_hex(raw_tx_0)
166          tx.vout[0].nValue -= int(4 * fee * COIN)  # Set more fee
167          self.check_mempool_result(
168              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'txn-mempool-conflict'}],
169              rawtxs=[tx.serialize().hex()],
170              maxfeerate=0,
171          )
172  
173          self.log.info('A transaction with missing inputs, that never existed')
174          tx = tx_from_hex(raw_tx_0)
175          tx.vin[0].prevout = COutPoint(hash=int('ff' * 32, 16), n=14)
176          self.check_mempool_result(
177              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'missing-inputs'}],
178              rawtxs=[tx.serialize().hex()],
179          )
180  
181          self.log.info('A transaction with missing inputs, that existed once in the past')
182          tx = tx_from_hex(raw_tx_0)
183          tx.vin[0].prevout.n = 1  # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend
184          raw_tx_1 = tx.serialize().hex()
185          txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, maxfeerate=0)
186          # Now spend both to "clearly hide" the outputs, ie. remove the coins from the utxo set by spending them
187          tx = self.wallet.create_self_transfer()['tx']
188          tx.vin.append(deepcopy(tx.vin[0]))
189          tx.wit.vtxinwit.append(deepcopy(tx.wit.vtxinwit[0]))
190          tx.vin[0].prevout = COutPoint(hash=int(txid_0, 16), n=0)
191          tx.vin[1].prevout = COutPoint(hash=int(txid_1, 16), n=0)
192          tx.vout[0].nValue = int(0.1 * COIN)
193          raw_tx_spend_both = tx.serialize().hex()
194          txid_spend_both = self.wallet.sendrawtransaction(from_node=node, tx_hex=raw_tx_spend_both)
195          self.generate(node, 1)
196          self.mempool_size = 0
197          # Now see if we can add the coins back to the utxo set by sending the exact txs again
198          self.check_mempool_result(
199              result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'missing-inputs'}],
200              rawtxs=[raw_tx_0],
201          )
202          self.check_mempool_result(
203              result_expected=[{'txid': txid_1, 'allowed': False, 'reject-reason': 'missing-inputs'}],
204              rawtxs=[raw_tx_1],
205          )
206  
207          self.log.info('Create a "reference" tx for later use')
208          utxo_to_spend = self.wallet.get_utxo(txid=txid_spend_both)
209          tx = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_spend, sequence=SEQUENCE_FINAL)['tx']
210          tx.vout[0].nValue = int(0.05 * COIN)
211          raw_tx_reference = tx.serialize().hex()
212          # Reference tx should be valid on itself
213          self.check_mempool_result(
214              result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.1') - Decimal('0.05')}}],
215              rawtxs=[tx.serialize().hex()],
216              maxfeerate=0,
217          )
218  
219          self.log.info('A transaction with no outputs')
220          tx = tx_from_hex(raw_tx_reference)
221          tx.vout = []
222          self.check_mempool_result(
223              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-empty'}],
224              rawtxs=[tx.serialize().hex()],
225          )
226  
227          self.log.info('A really large transaction')
228          tx = tx_from_hex(raw_tx_reference)
229          tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_WEIGHT // 4 / len(tx.vin[0].serialize()))
230          self.check_mempool_result(
231              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-oversize'}],
232              rawtxs=[tx.serialize().hex()],
233          )
234  
235          self.log.info('A transaction with negative output value')
236          tx = tx_from_hex(raw_tx_reference)
237          tx.vout[0].nValue *= -1
238          self.check_mempool_result(
239              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-negative'}],
240              rawtxs=[tx.serialize().hex()],
241          )
242  
243          # The following two validations prevent overflow of the output amounts (see CVE-2010-5139).
244          self.log.info('A transaction with too large output value')
245          tx = tx_from_hex(raw_tx_reference)
246          tx.vout[0].nValue = MAX_MONEY + 1
247          self.check_mempool_result(
248              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-vout-toolarge'}],
249              rawtxs=[tx.serialize().hex()],
250          )
251  
252          self.log.info('A transaction with too large sum of output values')
253          tx = tx_from_hex(raw_tx_reference)
254          tx.vout = [tx.vout[0]] * 2
255          tx.vout[0].nValue = MAX_MONEY
256          self.check_mempool_result(
257              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-txouttotal-toolarge'}],
258              rawtxs=[tx.serialize().hex()],
259          )
260  
261          self.log.info('A transaction with duplicate inputs')
262          tx = tx_from_hex(raw_tx_reference)
263          tx.vin = [tx.vin[0]] * 2
264          self.check_mempool_result(
265              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-inputs-duplicate'}],
266              rawtxs=[tx.serialize().hex()],
267          )
268  
269          self.log.info('A non-coinbase transaction with coinbase-like outpoint')
270          tx = tx_from_hex(raw_tx_reference)
271          tx.vin.append(CTxIn(COutPoint(hash=0, n=0xffffffff)))
272          self.check_mempool_result(
273              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-prevout-null'}],
274              rawtxs=[tx.serialize().hex()],
275          )
276  
277          self.log.info('A coinbase transaction')
278          # Pick the input of the first tx we created, so it has to be a coinbase tx
279          raw_tx_coinbase_spent = node.getrawtransaction(txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid'])
280          tx = tx_from_hex(raw_tx_coinbase_spent)
281          self.check_mempool_result(
282              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'coinbase'}],
283              rawtxs=[tx.serialize().hex()],
284          )
285  
286          self.log.info('Some nonstandard transactions')
287          tx = tx_from_hex(raw_tx_reference)
288          tx.nVersion = 3  # A version currently non-standard
289          self.check_mempool_result(
290              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}],
291              rawtxs=[tx.serialize().hex()],
292          )
293          tx = tx_from_hex(raw_tx_reference)
294          tx.vout[0].scriptPubKey = CScript([OP_0])  # Some non-standard script
295          self.check_mempool_result(
296              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptpubkey'}],
297              rawtxs=[tx.serialize().hex()],
298          )
299          tx = tx_from_hex(raw_tx_reference)
300          _, pubkey = generate_keypair()
301          tx.vout[0].scriptPubKey = keys_to_multisig_script([pubkey] * 3, k=2)  # Some bare multisig script (2-of-3)
302          self.check_mempool_result(
303              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bare-multisig'}],
304              rawtxs=[tx.serialize().hex()],
305          )
306          tx = tx_from_hex(raw_tx_reference)
307          tx.vin[0].scriptSig = CScript([OP_HASH160])  # Some not-pushonly scriptSig
308          self.check_mempool_result(
309              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-not-pushonly'}],
310              rawtxs=[tx.serialize().hex()],
311          )
312          tx = tx_from_hex(raw_tx_reference)
313          tx.vin[0].scriptSig = CScript([b'a' * 1648]) # Some too large scriptSig (>1650 bytes)
314          self.check_mempool_result(
315              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptsig-size'}],
316              rawtxs=[tx.serialize().hex()],
317          )
318          tx = tx_from_hex(raw_tx_reference)
319          output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=script_to_p2sh_script(b'burn'))
320          num_scripts = 100000 // len(output_p2sh_burn.serialize())  # Use enough outputs to make the tx too large for our policy
321          tx.vout = [output_p2sh_burn] * num_scripts
322          self.check_mempool_result(
323              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size'}],
324              rawtxs=[tx.serialize().hex()],
325          )
326          tx = tx_from_hex(raw_tx_reference)
327          tx.vout[0] = output_p2sh_burn
328          tx.vout[0].nValue -= 1  # Make output smaller, such that it is dust for our policy
329          self.check_mempool_result(
330              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'dust'}],
331              rawtxs=[tx.serialize().hex()],
332          )
333          tx = tx_from_hex(raw_tx_reference)
334          tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'\xff'])
335          tx.vout = [tx.vout[0]] * 2
336          self.check_mempool_result(
337              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'multi-op-return'}],
338              rawtxs=[tx.serialize().hex()],
339          )
340  
341          self.log.info('A timelocked transaction')
342          tx = tx_from_hex(raw_tx_reference)
343          tx.vin[0].nSequence -= 1  # Should be non-max, so locktime is not ignored
344          tx.nLockTime = node.getblockcount() + 1
345          self.check_mempool_result(
346              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-final'}],
347              rawtxs=[tx.serialize().hex()],
348          )
349  
350          self.log.info('A transaction that is locked by BIP68 sequence logic')
351          tx = tx_from_hex(raw_tx_reference)
352          tx.vin[0].nSequence = 2  # We could include it in the second block mined from now, but not the very next one
353          self.check_mempool_result(
354              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-BIP68-final'}],
355              rawtxs=[tx.serialize().hex()],
356              maxfeerate=0,
357          )
358  
359          # Prep for tiny-tx tests with wsh(OP_TRUE) output
360          seed_tx = self.wallet.send_to(from_node=node, scriptPubKey=script_to_p2wsh_script(CScript([OP_TRUE])), amount=COIN)
361          self.generate(node, 1)
362  
363          self.log.info('A tiny transaction(in non-witness bytes) that is disallowed')
364          tx = CTransaction()
365          tx.vin.append(CTxIn(COutPoint(int(seed_tx["txid"], 16), seed_tx["sent_vout"]), b"", SEQUENCE_FINAL))
366          tx.wit.vtxinwit = [CTxInWitness()]
367          tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
368          tx.vout.append(CTxOut(0, CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 2)))))
369          # Note it's only non-witness size that matters!
370          assert_equal(len(tx.serialize_without_witness()), 64)
371          assert_equal(MIN_STANDARD_TX_NONWITNESS_SIZE - 1, 64)
372          assert_greater_than(len(tx.serialize()), 64)
373  
374          self.check_mempool_result(
375              result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size-small'}],
376              rawtxs=[tx.serialize().hex()],
377              maxfeerate=0,
378          )
379  
380          self.log.info('Minimally-small transaction(in non-witness bytes) that is allowed')
381          tx.vout[0] = CTxOut(COIN - 1000, DUMMY_MIN_OP_RETURN_SCRIPT)
382          assert_equal(len(tx.serialize_without_witness()), MIN_STANDARD_TX_NONWITNESS_SIZE)
383          self.check_mempool_result(
384              result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.00001000')}}],
385              rawtxs=[tx.serialize().hex()],
386              maxfeerate=0,
387          )
388  
389  if __name__ == '__main__':
390      MempoolAcceptanceTest().main()