Skip to content

Commit 5a5e616

Browse files
authored
Merge pull request nbd-wtf#12 from nbd-wtf/master
update miner so we can get to descriptor wallets.
2 parents 0d60503 + 3289ee2 commit 5a5e616

33 files changed

+4259
-1536
lines changed

miner

Lines changed: 210 additions & 247 deletions
Large diffs are not rendered by default.

miner_imports/test_framework/address.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
# Copyright (c) 2016-2021 The Bitcoin Core developers
2+
# Copyright (c) 2016-2022 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Encode and decode Bitcoin addresses.
@@ -20,8 +20,17 @@
2020
sha256,
2121
taproot_construct,
2222
)
23-
from .segwit_addr import encode_segwit_address
2423
from .util import assert_equal
24+
from test_framework.script_util import (
25+
keyhash_to_p2pkh_script,
26+
program_to_witness_script,
27+
scripthash_to_p2sh_script,
28+
)
29+
from test_framework.segwit_addr import (
30+
decode_segwit_address,
31+
encode_segwit_address,
32+
)
33+
2534

2635
ADDRESS_BCRT1_UNSPENDABLE = 'bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj'
2736
ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR = 'addr(bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj)#juyq9d97'
@@ -35,22 +44,23 @@ class AddressType(enum.Enum):
3544
legacy = 'legacy' # P2PKH
3645

3746

38-
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
47+
b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
3948

4049

41-
def create_deterministic_address_bcrt1_p2tr_op_true():
50+
def create_deterministic_address_bcrt1_p2tr_op_true(explicit_internal_key=None):
4251
"""
4352
Generates a deterministic bech32m address (segwit v1 output) that
4453
can be spent with a witness stack of OP_TRUE and the control block
4554
with internal public key (script-path spending).
4655
47-
Returns a tuple with the generated address and the internal key.
56+
Returns a tuple with the generated address and the TaprootInfo object.
4857
"""
49-
internal_key = (1).to_bytes(32, 'big')
50-
scriptPubKey = taproot_construct(internal_key, [(None, CScript([OP_TRUE]))]).scriptPubKey
51-
address = encode_segwit_address("bcrt", 1, scriptPubKey[2:])
52-
assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka')
53-
return (address, internal_key)
58+
internal_key = explicit_internal_key or (1).to_bytes(32, 'big')
59+
taproot_info = taproot_construct(internal_key, [("only-path", CScript([OP_TRUE]))])
60+
address = output_key_to_p2tr(taproot_info.output_pubkey)
61+
if explicit_internal_key is None:
62+
assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka')
63+
return (address, taproot_info)
5464

5565

5666
def byte_to_base58(b, version):
@@ -59,10 +69,10 @@ def byte_to_base58(b, version):
5969
b += hash256(b)[:4] # append checksum
6070
value = int.from_bytes(b, 'big')
6171
while value > 0:
62-
result = chars[value % 58] + result
72+
result = b58chars[value % 58] + result
6373
value //= 58
6474
while b[0] == 0:
65-
result = chars[0] + result
75+
result = b58chars[0] + result
6676
b = b[1:]
6777
return result
6878

@@ -76,23 +86,23 @@ def base58_to_byte(s):
7686
n = 0
7787
for c in s:
7888
n *= 58
79-
assert c in chars
80-
digit = chars.index(c)
89+
assert c in b58chars
90+
digit = b58chars.index(c)
8191
n += digit
8292
h = '%x' % n
8393
if len(h) % 2:
8494
h = '0' + h
8595
res = n.to_bytes((n.bit_length() + 7) // 8, 'big')
8696
pad = 0
8797
for c in s:
88-
if c == chars[0]:
98+
if c == b58chars[0]:
8999
pad += 1
90100
else:
91101
break
92102
res = b'\x00' * pad + res
93103

94-
# Assert if the checksum is invalid
95-
assert_equal(hash256(res[:-4])[:4], res[-4:])
104+
if hash256(res[:-4])[:4] != res[-4:]:
105+
raise ValueError('Invalid Base58Check checksum')
96106

97107
return res[1:-4], int(res[0])
98108

@@ -141,6 +151,13 @@ def script_to_p2sh_p2wsh(script, main=False):
141151
p2shscript = CScript([OP_0, sha256(script)])
142152
return script_to_p2sh(p2shscript, main)
143153

154+
def output_key_to_p2tr(key, main=False):
155+
assert len(key) == 32
156+
return program_to_witness(1, key, main)
157+
158+
def p2a(main=False):
159+
return program_to_witness(1, "4e73", main)
160+
144161
def check_key(key):
145162
if (type(key) is str):
146163
key = bytes.fromhex(key) # Assuming this is hex string
@@ -156,6 +173,31 @@ def check_script(script):
156173
assert False
157174

158175

176+
def bech32_to_bytes(address):
177+
hrp = address.split('1')[0]
178+
if hrp not in ['bc', 'tb', 'bcrt']:
179+
return (None, None)
180+
version, payload = decode_segwit_address(hrp, address)
181+
if version is None:
182+
return (None, None)
183+
return version, bytearray(payload)
184+
185+
186+
def address_to_scriptpubkey(address):
187+
"""Converts a given address to the corresponding output script (scriptPubKey)."""
188+
version, payload = bech32_to_bytes(address)
189+
if version is not None:
190+
return program_to_witness_script(version, payload) # testnet segwit scriptpubkey
191+
payload, version = base58_to_byte(address)
192+
if version == 111: # testnet pubkey hash
193+
return keyhash_to_p2pkh_script(payload)
194+
elif version == 196: # testnet script hash
195+
return scripthash_to_p2sh_script(payload)
196+
# TODO: also support other address formats
197+
else:
198+
assert False
199+
200+
159201
class TestFrameworkScript(unittest.TestCase):
160202
def test_base58encodedecode(self):
161203
def check_base58(data, version):
@@ -173,3 +215,18 @@ def check_base58(data, version):
173215
check_base58(bytes.fromhex('0041c1eaf111802559bad61b60d62b1f897c63928a'), 0)
174216
check_base58(bytes.fromhex('000041c1eaf111802559bad61b60d62b1f897c63928a'), 0)
175217
check_base58(bytes.fromhex('00000041c1eaf111802559bad61b60d62b1f897c63928a'), 0)
218+
219+
220+
def test_bech32_decode(self):
221+
def check_bech32_decode(payload, version):
222+
hrp = "tb"
223+
self.assertEqual(bech32_to_bytes(encode_segwit_address(hrp, version, payload)), (version, payload))
224+
225+
check_bech32_decode(bytes.fromhex('36e3e2a33f328de12e4b43c515a75fba2632ecc3'), 0)
226+
check_bech32_decode(bytes.fromhex('823e9790fc1d1782321140d4f4aa61aabd5e045b'), 0)
227+
check_bech32_decode(bytes.fromhex('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), 1)
228+
check_bech32_decode(bytes.fromhex('39cf8ebd95134f431c39db0220770bd127f5dd3cc103c988b7dcd577ae34e354'), 1)
229+
check_bech32_decode(bytes.fromhex('708244006d27c757f6f1fc6f853b6ec26268b727866f7ce632886e34eb5839a3'), 1)
230+
check_bech32_decode(bytes.fromhex('616211ab00dffe0adcb6ce258d6d3fd8cbd901e2'), 0)
231+
check_bech32_decode(bytes.fromhex('b6a7c98b482d7fb21c9fa8e65692a0890410ff22'), 0)
232+
check_bech32_decode(bytes.fromhex('f0c2109cb1008cfa7b5a09cc56f7267cd8e50929'), 0)

miner_imports/test_framework/authproxy.py

Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
2727
- HTTP connections persist for the life of the AuthServiceProxy object
2828
(if server supports HTTP/1.1)
29-
- sends protocol 'version', per JSON-RPC 1.1
29+
- sends "jsonrpc":"2.0", per JSON-RPC 2.0
3030
- sends proper, incrementing 'id'
3131
- sends Basic HTTP authentication headers
3232
- parses all JSON numbers that look like floats as Decimal
@@ -39,7 +39,7 @@
3939
import http.client
4040
import json
4141
import logging
42-
import os
42+
import pathlib
4343
import socket
4444
import time
4545
import urllib.parse
@@ -60,9 +60,11 @@ def __init__(self, rpc_error, http_status=None):
6060
self.http_status = http_status
6161

6262

63-
def EncodeDecimal(o):
63+
def serialization_fallback(o):
6464
if isinstance(o, decimal.Decimal):
6565
return str(o)
66+
if isinstance(o, pathlib.Path):
67+
return str(o)
6668
raise TypeError(repr(o) + " is not JSON serializable")
6769

6870
class AuthServiceProxy():
@@ -78,7 +80,10 @@ def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connect
7880
passwd = None if self.__url.password is None else self.__url.password.encode('utf8')
7981
authpair = user + b':' + passwd
8082
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
81-
self.timeout = timeout
83+
# clamp the socket timeout, since larger values can cause an
84+
# "Invalid argument" exception in Python's HTTP(S) client
85+
# library on some operating systems (e.g. OpenBSD, FreeBSD)
86+
self.timeout = min(timeout, 2147483)
8287
self._set_conn(connection)
8388

8489
def __getattr__(self, name):
@@ -91,73 +96,71 @@ def __getattr__(self, name):
9196

9297
def _request(self, method, path, postdata):
9398
'''
94-
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
95-
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
99+
Do a HTTP request.
96100
'''
97101
headers = {'Host': self.__url.hostname,
98102
'User-Agent': USER_AGENT,
99103
'Authorization': self.__auth_header,
100104
'Content-type': 'application/json'}
101-
if os.name == 'nt':
102-
# Windows somehow does not like to re-use connections
103-
# TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows
104-
# Avoid "ConnectionAbortedError: [WinError 10053] An established connection was aborted by the software in your host machine"
105-
self._set_conn()
106-
try:
107-
self.__conn.request(method, path, postdata, headers)
108-
return self._get_response()
109-
except (BrokenPipeError, ConnectionResetError):
110-
# Python 3.5+ raises BrokenPipeError when the connection was reset
111-
# ConnectionResetError happens on FreeBSD
112-
self.__conn.close()
113-
self.__conn.request(method, path, postdata, headers)
114-
return self._get_response()
115-
except OSError as e:
116-
# Workaround for a bug on macOS. See https://bugs.python.org/issue33450
117-
retry = '[Errno 41] Protocol wrong type for socket' in str(e)
118-
if retry:
119-
self.__conn.close()
120-
self.__conn.request(method, path, postdata, headers)
121-
return self._get_response()
122-
else:
123-
raise
105+
self.__conn.request(method, path, postdata, headers)
106+
return self._get_response()
107+
108+
def _json_dumps(self, obj):
109+
return json.dumps(obj, default=serialization_fallback, ensure_ascii=self.ensure_ascii)
124110

125111
def get_request(self, *args, **argsn):
126112
AuthServiceProxy.__id_count += 1
127113

128-
log.debug("-{}-> {} {}".format(
114+
log.debug("-{}-> {} {} {}".format(
129115
AuthServiceProxy.__id_count,
130116
self._service_name,
131-
json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii),
117+
self._json_dumps(args),
118+
self._json_dumps(argsn),
132119
))
120+
133121
if args and argsn:
134-
raise ValueError('Cannot handle both named and positional arguments')
135-
return {'version': '1.1',
122+
params = dict(args=args, **argsn)
123+
else:
124+
params = args or argsn
125+
return {'jsonrpc': '2.0',
136126
'method': self._service_name,
137-
'params': args or argsn,
127+
'params': params,
138128
'id': AuthServiceProxy.__id_count}
139129

140130
def __call__(self, *args, **argsn):
141-
postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
131+
postdata = self._json_dumps(self.get_request(*args, **argsn))
142132
response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
143-
if response['error'] is not None:
144-
raise JSONRPCException(response['error'], status)
145-
elif 'result' not in response:
146-
raise JSONRPCException({
147-
'code': -343, 'message': 'missing JSON-RPC result'}, status)
148-
elif status != HTTPStatus.OK:
149-
raise JSONRPCException({
150-
'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
133+
# For backwards compatibility tests, accept JSON RPC 1.1 responses
134+
if 'jsonrpc' not in response:
135+
if response['error'] is not None:
136+
raise JSONRPCException(response['error'], status)
137+
elif 'result' not in response:
138+
raise JSONRPCException({
139+
'code': -343, 'message': 'missing JSON-RPC result'}, status)
140+
elif status != HTTPStatus.OK:
141+
raise JSONRPCException({
142+
'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
143+
else:
144+
return response['result']
151145
else:
146+
assert response['jsonrpc'] == '2.0'
147+
if status != HTTPStatus.OK:
148+
raise JSONRPCException({
149+
'code': -342, 'message': 'non-200 HTTP status code'}, status)
150+
if 'error' in response:
151+
raise JSONRPCException(response['error'], status)
152+
elif 'result' not in response:
153+
raise JSONRPCException({
154+
'code': -343, 'message': 'missing JSON-RPC 2.0 result and error'}, status)
152155
return response['result']
153156

154157
def batch(self, rpc_call_list):
155-
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
158+
postdata = self._json_dumps(list(rpc_call_list))
156159
log.debug("--> " + postdata)
157160
response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
158161
if status != HTTPStatus.OK:
159162
raise JSONRPCException({
160-
'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
163+
'code': -342, 'message': 'non-200 HTTP status code'}, status)
161164
return response
162165

163166
def _get_response(self):
@@ -175,17 +178,31 @@ def _get_response(self):
175178
raise JSONRPCException({
176179
'code': -342, 'message': 'missing HTTP response from server'})
177180

181+
# Check for no-content HTTP status code, which can be returned when an
182+
# RPC client requests a JSON-RPC 2.0 "notification" with no response.
183+
# Currently this is only possible if clients call the _request() method
184+
# directly to send a raw request.
185+
if http_response.status == HTTPStatus.NO_CONTENT:
186+
if len(http_response.read()) != 0:
187+
raise JSONRPCException({'code': -342, 'message': 'Content received with NO CONTENT status code'})
188+
return None, http_response.status
189+
178190
content_type = http_response.getheader('Content-Type')
179191
if content_type != 'application/json':
180192
raise JSONRPCException(
181193
{'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)},
182194
http_response.status)
183195

184-
responsedata = http_response.read().decode('utf8')
196+
data = http_response.read()
197+
try:
198+
responsedata = data.decode('utf8')
199+
except UnicodeDecodeError as e:
200+
raise JSONRPCException({
201+
'code': -342, 'message': f'Cannot decode response in utf8 format, content: {data}, exception: {e}'})
185202
response = json.loads(responsedata, parse_float=decimal.Decimal)
186203
elapsed = time.time() - req_start_time
187204
if "error" in response and response["error"] is None:
188-
log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
205+
log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, self._json_dumps(response["result"])))
189206
else:
190207
log.debug("<-- [%.6f] %s" % (elapsed, responsedata))
191208
return response, http_response.status

0 commit comments

Comments
 (0)