/ test / functional / tool_signet_miner.py
tool_signet_miner.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 signet miner tool"""
  6  
  7  import json
  8  import os.path
  9  import shlex
 10  import subprocess
 11  import sys
 12  import time
 13  
 14  from test_framework.blocktools import DIFF_1_N_BITS, SIGNET_HEADER
 15  from test_framework.key import ECKey
 16  from test_framework.script_util import CScript, key_to_p2wpkh_script
 17  from test_framework.test_framework import BitcoinTestFramework
 18  from test_framework.util import (
 19      assert_equal,
 20      wallet_importprivkey,
 21  )
 22  from test_framework.wallet_util import bytes_to_wif
 23  
 24  
 25  CHALLENGE_PRIVATE_KEY = (42).to_bytes(32, 'big')
 26  
 27  def get_segwit_commitment(node):
 28      coinbase = node.getblock(node.getbestblockhash(), 2)['tx'][0]
 29      commitment = coinbase['vout'][1]['scriptPubKey']['hex']
 30      assert_equal(commitment[0:12], '6a24aa21a9ed')
 31      return commitment
 32  
 33  def get_signet_commitment(segwit_commitment):
 34      for el in CScript.fromhex(segwit_commitment):
 35          if isinstance(el, bytes) and el[0:4] == SIGNET_HEADER:
 36              return el[4:].hex()
 37      return None
 38  
 39  class SignetMinerTest(BitcoinTestFramework):
 40      def set_test_params(self):
 41          self.chain = "signet"
 42          self.setup_clean_chain = True
 43          self.num_nodes = 4
 44  
 45          # generate and specify signet challenge (simple p2wpkh script)
 46          privkey = ECKey()
 47          privkey.set(CHALLENGE_PRIVATE_KEY, True)
 48          pubkey = privkey.get_pubkey().get_bytes()
 49          challenge = key_to_p2wpkh_script(pubkey)
 50  
 51          self.extra_args = [
 52              [f'-signetchallenge={challenge.hex()}'],
 53              ["-signetchallenge=51"], # OP_TRUE
 54              ["-signetchallenge=60"], # OP_16
 55              ["-signetchallenge=202cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"], # sha256("hello")
 56          ]
 57  
 58      def skip_test_if_missing_module(self):
 59          self.skip_if_no_cli()
 60          self.skip_if_no_wallet()
 61          self.skip_if_no_bitcoin_util()
 62  
 63      def setup_network(self):
 64          self.setup_nodes()
 65          # Nodes with different signet networks are not connected
 66  
 67      # generate block with signet miner tool
 68      def mine_block(self, node):
 69          n_blocks = node.getblockcount()
 70          base_dir = self.config["environment"]["SRCDIR"]
 71          signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
 72          rpc_argv = node.binaries.rpc_argv() + [f"-datadir={node.cli.datadir}"]
 73          util_argv = node.binaries.util_argv() + ["grind"]
 74          subprocess.run([
 75                  sys.executable,
 76                  signet_miner_path,
 77                  f'--cli={shlex.join(rpc_argv)}',
 78                  'generate',
 79                  f'--address={node.getnewaddress()}',
 80                  f'--grind-cmd={shlex.join(util_argv)}',
 81                  f'--nbits={DIFF_1_N_BITS:08x}',
 82                  f'--set-block-time={int(time.time())}',
 83                  '--poolnum=99',
 84              ], check=True, stderr=subprocess.STDOUT)
 85          assert_equal(node.getblockcount(), n_blocks + 1)
 86  
 87      # generate block using the signet miner tool genpsbt and solvepsbt commands
 88      def mine_block_manual(self, node, *, sign):
 89          n_blocks = node.getblockcount()
 90          base_dir = self.config["environment"]["SRCDIR"]
 91          signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner")
 92          rpc_argv = node.binaries.rpc_argv() + [f"-datadir={node.cli.datadir}"]
 93          util_argv = node.binaries.util_argv() + ["grind"]
 94          base_cmd = [
 95              sys.executable,
 96              signet_miner_path,
 97              f'--cli={shlex.join(rpc_argv)}',
 98          ]
 99  
100          template = node.getblocktemplate(dict(rules=["signet","segwit"]))
101          genpsbt = subprocess.run(base_cmd + [
102                  'genpsbt',
103                  f'--address={node.getnewaddress()}',
104                  '--poolnum=98',
105              ], check=True, text=True, input=json.dumps(template), capture_output=True)
106          psbt = genpsbt.stdout.strip()
107          if sign:
108              self.log.debug("Sign the PSBT")
109              res = node.walletprocesspsbt(psbt=psbt, sign=True, sighashtype='ALL')
110              assert res['complete']
111              psbt = res['psbt']
112          solvepsbt = subprocess.run(base_cmd + [
113                  'solvepsbt',
114                  f'--grind-cmd={shlex.join(util_argv)}',
115              ], check=True, text=True, input=psbt, capture_output=True)
116          node.submitblock(solvepsbt.stdout.strip())
117          assert_equal(node.getblockcount(), n_blocks + 1)
118  
119      def run_test(self):
120          self.log.info("Signet node with single signature challenge")
121          node = self.nodes[0]
122          # import private key needed for signing block
123          wallet_importprivkey(node, bytes_to_wif(CHALLENGE_PRIVATE_KEY), 0)
124          self.mine_block(node)
125          # MUST include signet commitment
126          assert get_signet_commitment(get_segwit_commitment(node))
127  
128          self.log.info("Mine manually using genpsbt and solvepsbt")
129          self.mine_block_manual(node, sign=True)
130          assert get_signet_commitment(get_segwit_commitment(node))
131  
132          node = self.nodes[1]
133          self.log.info("Signet node with trivial challenge (OP_TRUE)")
134          self.mine_block(node)
135          # MAY omit signet commitment (BIP 325). Do so for better compatibility
136          # with signet unaware mining software and hardware.
137          assert get_signet_commitment(get_segwit_commitment(node)) is None
138  
139          node = self.nodes[2]
140          self.log.info("Signet node with trivial challenge (OP_16)")
141          self.mine_block(node)
142          assert get_signet_commitment(get_segwit_commitment(node)) is None
143  
144          node = self.nodes[3]
145          self.log.info("Signet node with trivial challenge (push sha256 hash)")
146          self.mine_block(node)
147          assert get_signet_commitment(get_segwit_commitment(node)) is None
148  
149          self.log.info("Manual mining with a trivial challenge doesn't require a PSBT")
150          self.mine_block_manual(node, sign=False)
151          assert get_signet_commitment(get_segwit_commitment(node)) is None
152  
153  
154  if __name__ == "__main__":
155      SignetMinerTest(__file__).main()