generate-seeds.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-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  '''
   6  Script to generate list of seed nodes for kernel/chainparams.cpp.
   7  
   8  This script expects three text files in the directory that is passed as an
   9  argument:
  10  
  11      nodes_main.txt
  12      nodes_signet.txt
  13      nodes_test.txt
  14      nodes_testnet4.txt
  15  
  16  These files must consist of lines in the format
  17  
  18      <ip>:<port>
  19      [<ipv6>]:<port>
  20      <onion>.onion:<port>
  21      <i2p>.b32.i2p:<port>
  22  
  23  The output will be several data structures with the peers in binary format:
  24  
  25     static const uint8_t chainparams_seed_{main,signet,test,testnet4}[]={
  26     ...
  27     }
  28  
  29  These should be pasted into `src/chainparamsseeds.h`.
  30  '''
  31  
  32  from base64 import b32decode
  33  from enum import Enum
  34  import sys
  35  import os
  36  import re
  37  
  38  class BIP155Network(Enum):
  39      IPV4 = 1
  40      IPV6 = 2
  41      TORV2 = 3  # no longer supported
  42      TORV3 = 4
  43      I2P = 5
  44      CJDNS = 6
  45  
  46  def name_to_bip155(addr):
  47      '''Convert address string to BIP155 (networkID, addr) tuple.'''
  48      if addr.endswith('.onion'):
  49          vchAddr = b32decode(addr[0:-6], True)
  50          if len(vchAddr) == 35:
  51              assert vchAddr[34] == 3
  52              return (BIP155Network.TORV3, vchAddr[:32])
  53          elif len(vchAddr) == 10:
  54              return (BIP155Network.TORV2, vchAddr)
  55          else:
  56              raise ValueError('Invalid onion %s' % vchAddr)
  57      elif addr.endswith('.b32.i2p'):
  58          vchAddr = b32decode(addr[0:-8] + '====', True)
  59          if len(vchAddr) == 32:
  60              return (BIP155Network.I2P, vchAddr)
  61          else:
  62              raise ValueError(f'Invalid I2P {vchAddr}')
  63      elif '.' in addr: # IPv4
  64          return (BIP155Network.IPV4, bytes((int(x) for x in addr.split('.'))))
  65      elif ':' in addr: # IPv6 or CJDNS
  66          sub = [[], []] # prefix, suffix
  67          x = 0
  68          addr = addr.split(':')
  69          for i,comp in enumerate(addr):
  70              if comp == '':
  71                  if i == 0 or i == (len(addr)-1): # skip empty component at beginning or end
  72                      continue
  73                  x += 1 # :: skips to suffix
  74                  assert x < 2
  75              else: # two bytes per component
  76                  val = int(comp, 16)
  77                  sub[x].append(val >> 8)
  78                  sub[x].append(val & 0xff)
  79          nullbytes = 16 - len(sub[0]) - len(sub[1])
  80          assert (x == 0 and nullbytes == 0) or (x == 1 and nullbytes > 0)
  81          addr_bytes = bytes(sub[0] + ([0] * nullbytes) + sub[1])
  82          if addr_bytes[0] == 0xfc:
  83              # Assume that seeds with fc00::/8 addresses belong to CJDNS,
  84              # not to the publicly unroutable "Unique Local Unicast" network, see
  85              # RFC4193: https://datatracker.ietf.org/doc/html/rfc4193#section-8
  86              return (BIP155Network.CJDNS, addr_bytes)
  87          else:
  88              return (BIP155Network.IPV6, addr_bytes)
  89      else:
  90          raise ValueError('Could not parse address %s' % addr)
  91  
  92  def parse_spec(s):
  93      '''Convert endpoint string to BIP155 (networkID, addr, port) tuple.'''
  94      match = re.match(r'\[([0-9a-fA-F:]+)\](?::([0-9]+))?$', s)
  95      if match: # ipv6
  96          host = match.group(1)
  97          port = match.group(2)
  98      elif s.count(':') > 1: # ipv6, no port
  99          host = s
 100          port = ''
 101      else:
 102          (host,_,port) = s.partition(':')
 103  
 104      if not port:
 105          port = 0
 106      else:
 107          port = int(port)
 108  
 109      host = name_to_bip155(host)
 110  
 111      if host[0] == BIP155Network.TORV2:
 112          return None  # TORV2 is no longer supported, so we ignore it
 113      else:
 114          return host + (port, )
 115  
 116  def ser_compact_size(l):
 117      r = b""
 118      if l < 253:
 119          r = l.to_bytes(1, "little")
 120      elif l < 0x10000:
 121          r = (253).to_bytes(1, "little") + l.to_bytes(2, "little")
 122      elif l < 0x100000000:
 123          r = (254).to_bytes(1, "little") + l.to_bytes(4, "little")
 124      else:
 125          r = (255).to_bytes(1, "little") + l.to_bytes(8, "little")
 126      return r
 127  
 128  def bip155_serialize(spec):
 129      '''
 130      Serialize (networkID, addr, port) tuple to BIP155 binary format.
 131      '''
 132      r = b""
 133      r += spec[0].value.to_bytes(1, "little")
 134      r += ser_compact_size(len(spec[1]))
 135      r += spec[1]
 136      r += spec[2].to_bytes(2, "big")
 137      return r
 138  
 139  def process_nodes(g, f, structname):
 140      g.write('static const uint8_t %s[] = {\n' % structname)
 141      for line in f:
 142          comment = line.find('#')
 143          if comment != -1:
 144              line = line[0:comment]
 145          line = line.strip()
 146          if not line:
 147              continue
 148  
 149          spec = parse_spec(line)
 150          if spec is None:  # ignore this entry (e.g. no longer supported addresses like TORV2)
 151              continue
 152          blob = bip155_serialize(spec)
 153          hoststr = ','.join(('0x%02x' % b) for b in blob)
 154          g.write(f'    {hoststr},\n')
 155      g.write('};\n')
 156  
 157  def main():
 158      if len(sys.argv)<2:
 159          print(('Usage: %s <path_to_nodes_txt>' % sys.argv[0]), file=sys.stderr)
 160          sys.exit(1)
 161      g = sys.stdout
 162      indir = sys.argv[1]
 163      g.write('// Copyright (c) The Bitcoin Core developers\n')
 164      g.write('// Distributed under the MIT software license, see the accompanying\n')
 165      g.write('// file COPYING or https://opensource.org/license/mit.\n\n')
 166      g.write('#ifndef BITCOIN_CHAINPARAMSSEEDS_H\n')
 167      g.write('#define BITCOIN_CHAINPARAMSSEEDS_H\n')
 168      g.write('/**\n')
 169      g.write(' * List of fixed seed nodes for the bitcoin network\n')
 170      g.write(' * AUTOGENERATED by contrib/seeds/generate-seeds.py\n')
 171      g.write(' *\n')
 172      g.write(' * Each line contains a BIP155 serialized (networkID, addr, port) tuple.\n')
 173      g.write(' */\n')
 174      with open(os.path.join(indir,'nodes_main.txt'), 'r') as f:
 175          process_nodes(g, f, 'chainparams_seed_main')
 176      g.write('\n')
 177      with open(os.path.join(indir,'nodes_signet.txt'), 'r') as f:
 178          process_nodes(g, f, 'chainparams_seed_signet')
 179      g.write('\n')
 180      with open(os.path.join(indir,'nodes_test.txt'), 'r') as f:
 181          process_nodes(g, f, 'chainparams_seed_test')
 182      g.write('\n')
 183      with open(os.path.join(indir,'nodes_testnet4.txt'), 'r') as f:
 184          process_nodes(g, f, 'chainparams_seed_testnet4')
 185      g.write('#endif // BITCOIN_CHAINPARAMSSEEDS_H\n')
 186  
 187  if __name__ == '__main__':
 188      main()
 189