/ test / functional / mempool_dust.py
mempool_dust.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2022-present 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 dust limit mempool policy (`-dustrelayfee` parameter)"""
  6  from decimal import Decimal
  7  
  8  from test_framework.messages import (
  9      COIN,
 10      CTxOut,
 11  )
 12  from test_framework.script import (
 13      CScript,
 14      OP_RETURN,
 15      OP_TRUE,
 16  )
 17  from test_framework.script_util import (
 18      key_to_p2pk_script,
 19      key_to_p2pkh_script,
 20      key_to_p2wpkh_script,
 21      keys_to_multisig_script,
 22      output_key_to_p2tr_script,
 23      program_to_witness_script,
 24      script_to_p2sh_script,
 25      script_to_p2wsh_script,
 26  )
 27  from test_framework.test_framework import BitcoinTestFramework
 28  from test_framework.test_node import TestNode
 29  from test_framework.util import (
 30      assert_equal,
 31      get_fee,
 32  )
 33  from test_framework.wallet import MiniWallet
 34  from test_framework.wallet_util import generate_keypair
 35  
 36  
 37  DUST_RELAY_TX_FEE = 3000  # default setting [sat/kvB]
 38  
 39  
 40  class DustRelayFeeTest(BitcoinTestFramework):
 41      def set_test_params(self):
 42          self.num_nodes = 1
 43          self.extra_args = [['-permitbaremultisig']]
 44  
 45      def test_dust_output(self, node: TestNode, dust_relay_fee: Decimal,
 46                           output_script: CScript, type_desc: str) -> None:
 47          # determine dust threshold (see `GetDustThreshold`)
 48          if output_script[0] == OP_RETURN:
 49              dust_threshold = 0
 50          else:
 51              tx_size = len(CTxOut(nValue=0, scriptPubKey=output_script).serialize())
 52              tx_size += 67 if output_script.IsWitnessProgram() else 148
 53              dust_threshold = int(get_fee(tx_size, dust_relay_fee) * COIN)
 54          self.log.info(f"-> Test {type_desc} output (size {len(output_script)}, limit {dust_threshold})")
 55  
 56          # amount right on the dust threshold should pass
 57          tx = self.wallet.create_self_transfer()["tx"]
 58          tx.vout.append(CTxOut(nValue=dust_threshold, scriptPubKey=output_script))
 59          tx.vout[0].nValue -= dust_threshold  # keep total output value constant
 60          tx_good_hex = tx.serialize().hex()
 61          res = node.testmempoolaccept([tx_good_hex])[0]
 62          assert_equal(res['allowed'], True)
 63  
 64          # amount just below the dust threshold should fail
 65          if dust_threshold > 0:
 66              tx.vout[1].nValue -= 1
 67              res = node.testmempoolaccept([tx.serialize().hex()])[0]
 68              assert_equal(res['allowed'], False)
 69              assert_equal(res['reject-reason'], 'dust')
 70  
 71          # finally send the transaction to avoid running out of MiniWallet UTXOs
 72          self.wallet.sendrawtransaction(from_node=node, tx_hex=tx_good_hex)
 73  
 74      def test_dustrelay(self):
 75          self.log.info("Test that small outputs are acceptable when dust relay rate is set to 0 that would otherwise trigger ephemeral dust rules")
 76  
 77          self.restart_node(0, extra_args=["-dustrelayfee=0"])
 78  
 79          assert_equal(self.nodes[0].getrawmempool(), [])
 80  
 81          # Create two dust outputs. Transaction has zero fees. both dust outputs are unspent, and would have failed individual checks.
 82          # The amount is 1 satoshi because create_self_transfer_multi disallows 0.
 83          dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=1000, amount_per_output=1, num_outputs=2)
 84          dust_txid = self.nodes[0].sendrawtransaction(hexstring=dusty_tx["hex"], maxfeerate=0)
 85  
 86          assert_equal(self.nodes[0].getrawmempool(), [dust_txid])
 87  
 88          # Spends one dust along with fee input, leave other dust unspent to check ephemeral dust checks aren't being enforced
 89          sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[self.wallet.get_utxo(), dusty_tx["new_utxos"][0]])
 90          sweep_txid = self.nodes[0].sendrawtransaction(sweep_tx["hex"])
 91  
 92          mempool_entries = self.nodes[0].getrawmempool()
 93          assert dust_txid in mempool_entries
 94          assert sweep_txid in mempool_entries
 95          assert_equal(len(mempool_entries), 2)
 96  
 97          # Wipe extra arg to reset dust relay
 98          self.restart_node(0, extra_args=[])
 99  
100          assert_equal(self.nodes[0].getrawmempool(), [])
101  
102      def run_test(self):
103          self.wallet = MiniWallet(self.nodes[0])
104  
105          self.test_dustrelay()
106  
107          # prepare output scripts of each standard type
108          _, uncompressed_pubkey = generate_keypair(compressed=False)
109          _, pubkey = generate_keypair(compressed=True)
110  
111          output_scripts = (
112              (key_to_p2pk_script(uncompressed_pubkey),          "P2PK (uncompressed)"),
113              (key_to_p2pk_script(pubkey),                       "P2PK (compressed)"),
114              (key_to_p2pkh_script(pubkey),                      "P2PKH"),
115              (script_to_p2sh_script(CScript([OP_TRUE])),        "P2SH"),
116              (key_to_p2wpkh_script(pubkey),                     "P2WPKH"),
117              (script_to_p2wsh_script(CScript([OP_TRUE])),       "P2WSH"),
118              (output_key_to_p2tr_script(pubkey[1:]),            "P2TR"),
119              # witness programs for segwitv2+ can be between 2 and 40 bytes
120              (program_to_witness_script(2,  b'\x66' * 2),       "P2?? (future witness version 2)"),
121              (program_to_witness_script(16, b'\x77' * 40),      "P2?? (future witness version 16)"),
122              # largest possible output script considered standard
123              (keys_to_multisig_script([uncompressed_pubkey]*3), "bare multisig (m-of-3)"),
124              (CScript([OP_RETURN, b'superimportanthash']),      "null data (OP_RETURN)"),
125          )
126  
127          # test default (no parameter), disabled (=0) and a bunch of arbitrary dust fee rates [sat/kvB]
128          for dustfee_sat_kvb in (DUST_RELAY_TX_FEE, 0, 1, 66, 500, 1337, 12345, 21212, 333333):
129              dustfee_btc_kvb = dustfee_sat_kvb / Decimal(COIN)
130              if dustfee_sat_kvb == DUST_RELAY_TX_FEE:
131                  self.log.info(f"Test default dust limit setting ({dustfee_sat_kvb} sat/kvB)...")
132              else:
133                  dust_parameter = f"-dustrelayfee={dustfee_btc_kvb:.8f}"
134                  self.log.info(f"Test dust limit setting {dust_parameter} ({dustfee_sat_kvb} sat/kvB)...")
135                  self.restart_node(0, extra_args=[dust_parameter, "-permitbaremultisig"])
136  
137              for output_script, description in output_scripts:
138                  self.test_dust_output(self.nodes[0], dustfee_btc_kvb, output_script, description)
139              self.generate(self.nodes[0], 1)
140  
141  
142  if __name__ == '__main__':
143      DustRelayFeeTest(__file__).main()