aboutsummaryrefslogtreecommitdiff
path: root/contrib/seeds/generate-seeds.py
blob: e921757802adb915984e25a4eb1fb09590f37991 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
# Copyright (c) 2014-2021 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
'''
Script to generate list of seed nodes for kernel/chainparams.cpp.

This script expects two text files in the directory that is passed as an
argument:

    nodes_main.txt
    nodes_test.txt

These files must consist of lines in the format

    <ip>:<port>
    [<ipv6>]:<port>
    <onion>.onion:<port>
    <i2p>.b32.i2p:<port>

The output will be two data structures with the peers in binary format:

   static const uint8_t chainparams_seed_{main,test}[]={
   ...
   }

These should be pasted into `src/chainparamsseeds.h`.
'''

from base64 import b32decode
from enum import Enum
import struct
import sys
import os
import re

class BIP155Network(Enum):
    IPV4 = 1
    IPV6 = 2
    TORV2 = 3  # no longer supported
    TORV3 = 4
    I2P = 5
    CJDNS = 6

def name_to_bip155(addr):
    '''Convert address string to BIP155 (networkID, addr) tuple.'''
    if addr.endswith('.onion'):
        vchAddr = b32decode(addr[0:-6], True)
        if len(vchAddr) == 35:
            assert vchAddr[34] == 3
            return (BIP155Network.TORV3, vchAddr[:32])
        elif len(vchAddr) == 10:
            return (BIP155Network.TORV2, vchAddr)
        else:
            raise ValueError('Invalid onion %s' % vchAddr)
    elif addr.endswith('.b32.i2p'):
        vchAddr = b32decode(addr[0:-8] + '====', True)
        if len(vchAddr) == 32:
            return (BIP155Network.I2P, vchAddr)
        else:
            raise ValueError(f'Invalid I2P {vchAddr}')
    elif '.' in addr: # IPv4
        return (BIP155Network.IPV4, bytes((int(x) for x in addr.split('.'))))
    elif ':' in addr: # IPv6 or CJDNS
        sub = [[], []] # prefix, suffix
        x = 0
        addr = addr.split(':')
        for i,comp in enumerate(addr):
            if comp == '':
                if i == 0 or i == (len(addr)-1): # skip empty component at beginning or end
                    continue
                x += 1 # :: skips to suffix
                assert x < 2
            else: # two bytes per component
                val = int(comp, 16)
                sub[x].append(val >> 8)
                sub[x].append(val & 0xff)
        nullbytes = 16 - len(sub[0]) - len(sub[1])
        assert (x == 0 and nullbytes == 0) or (x == 1 and nullbytes > 0)
        addr_bytes = bytes(sub[0] + ([0] * nullbytes) + sub[1])
        if addr_bytes[0] == 0xfc:
            # Assume that seeds with fc00::/8 addresses belong to CJDNS,
            # not to the publicly unroutable "Unique Local Unicast" network, see
            # RFC4193: https://datatracker.ietf.org/doc/html/rfc4193#section-8
            return (BIP155Network.CJDNS, addr_bytes)
        else:
            return (BIP155Network.IPV6, addr_bytes)
    else:
        raise ValueError('Could not parse address %s' % addr)

def parse_spec(s):
    '''Convert endpoint string to BIP155 (networkID, addr, port) tuple.'''
    match = re.match(r'\[([0-9a-fA-F:]+)\](?::([0-9]+))?$', s)
    if match: # ipv6
        host = match.group(1)
        port = match.group(2)
    elif s.count(':') > 1: # ipv6, no port
        host = s
        port = ''
    else:
        (host,_,port) = s.partition(':')

    if not port:
        port = 0
    else:
        port = int(port)

    host = name_to_bip155(host)

    if host[0] == BIP155Network.TORV2:
        return None  # TORV2 is no longer supported, so we ignore it
    else:
        return host + (port, )

def ser_compact_size(l):
    r = b""
    if l < 253:
        r = struct.pack("B", l)
    elif l < 0x10000:
        r = struct.pack("<BH", 253, l)
    elif l < 0x100000000:
        r = struct.pack("<BI", 254, l)
    else:
        r = struct.pack("<BQ", 255, l)
    return r

def bip155_serialize(spec):
    '''
    Serialize (networkID, addr, port) tuple to BIP155 binary format.
    '''
    r = b""
    r += struct.pack('B', spec[0].value)
    r += ser_compact_size(len(spec[1]))
    r += spec[1]
    r += struct.pack('>H', spec[2])
    return r

def process_nodes(g, f, structname):
    g.write('static const uint8_t %s[] = {\n' % structname)
    for line in f:
        comment = line.find('#')
        if comment != -1:
            line = line[0:comment]
        line = line.strip()
        if not line:
            continue

        spec = parse_spec(line)
        if spec is None:  # ignore this entry (e.g. no longer supported addresses like TORV2)
            continue
        blob = bip155_serialize(spec)
        hoststr = ','.join(('0x%02x' % b) for b in blob)
        g.write(f'    {hoststr},\n')
    g.write('};\n')

def main():
    if len(sys.argv)<2:
        print(('Usage: %s <path_to_nodes_txt>' % sys.argv[0]), file=sys.stderr)
        sys.exit(1)
    g = sys.stdout
    indir = sys.argv[1]
    g.write('#ifndef BITCOIN_CHAINPARAMSSEEDS_H\n')
    g.write('#define BITCOIN_CHAINPARAMSSEEDS_H\n')
    g.write('/**\n')
    g.write(' * List of fixed seed nodes for the bitcoin network\n')
    g.write(' * AUTOGENERATED by contrib/seeds/generate-seeds.py\n')
    g.write(' *\n')
    g.write(' * Each line contains a BIP155 serialized (networkID, addr, port) tuple.\n')
    g.write(' */\n')
    with open(os.path.join(indir,'nodes_main.txt'), 'r', encoding="utf8") as f:
        process_nodes(g, f, 'chainparams_seed_main')
    g.write('\n')
    with open(os.path.join(indir,'nodes_test.txt'), 'r', encoding="utf8") as f:
        process_nodes(g, f, 'chainparams_seed_test')
    g.write('#endif // BITCOIN_CHAINPARAMSSEEDS_H\n')

if __name__ == '__main__':
    main()