tool_signet_miner.py raw

   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.datadir_path}"]
  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.datadir_path}"]
  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()
 156