Skip to content

Commit e57087d

Browse files
committed
interface: implement support for protocol 1.6
ref spesmilo/electrum-protocol#6
1 parent cd3173a commit e57087d

File tree

3 files changed

+65
-15
lines changed

3 files changed

+65
-15
lines changed

electrum/interface.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -868,13 +868,25 @@ async def get_block_headers(
868868
res = await self.session.send_request('blockchain.block.headers', [start_height, count], timeout=timeout)
869869
# check response
870870
assert_dict_contains_field(res, field_name='count')
871-
assert_dict_contains_field(res, field_name='hex')
872871
assert_dict_contains_field(res, field_name='max')
873872
assert_non_negative_integer(res['count'])
874873
assert_non_negative_integer(res['max'])
875-
assert_hex_str(res['hex'])
876-
if len(res['hex']) != HEADER_SIZE * 2 * res['count']:
877-
raise RequestCorrupted('inconsistent chunk hex and count')
874+
if self.active_protocol_tuple >= (1, 6):
875+
hex_headers_list = assert_dict_contains_field(res, field_name='headers')
876+
assert_list_or_tuple(hex_headers_list)
877+
for item in hex_headers_list:
878+
assert_hex_str(item)
879+
if len(item) != HEADER_SIZE * 2:
880+
raise RequestCorrupted(f"invalid header size. got {len(item)//2}, expected {HEADER_SIZE}")
881+
if len(hex_headers_list) != res['count']:
882+
raise RequestCorrupted(f"{len(hex_headers_list)=} != {res['count']=}")
883+
headers = list(bfh(hex_header) for hex_header in hex_headers_list)
884+
else: # proto 1.4
885+
hex_headers_concat = assert_dict_contains_field(res, field_name='hex')
886+
assert_hex_str(hex_headers_concat)
887+
if len(hex_headers_concat) != HEADER_SIZE * 2 * res['count']:
888+
raise RequestCorrupted('inconsistent chunk hex and count')
889+
headers = list(util.chunks(bfh(hex_headers_concat), size=HEADER_SIZE))
878890
# we never request more than MAX_NUM_HEADERS_IN_REQUEST headers, but we enforce those fit in a single response
879891
if res['max'] < MAX_NUM_HEADERS_PER_REQUEST:
880892
raise RequestCorrupted(f"server uses too low 'max' count for block.headers: {res['max']} < {MAX_NUM_HEADERS_PER_REQUEST}")
@@ -887,7 +899,6 @@ async def get_block_headers(
887899
raise RequestCorrupted(
888900
f"asked for {count} headers but got fewer: {res['count']}. ({start_height=}, {self.tip=})")
889901
# checks done.
890-
headers = list(util.chunks(bfh(res['hex']), size=HEADER_SIZE))
891902
return headers
892903

893904
async def request_chunk_below_max_checkpoint(
@@ -1405,6 +1416,33 @@ async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> Non
14051416
# the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
14061417
self._rawtx_cache[txid_calc] = bytes.fromhex(rawtx)
14071418

1419+
async def broadcast_txpackage(self, txs: Sequence['Transaction']) -> bool:
1420+
assert self.active_protocol_tuple >= (1, 6), f"server using old protocol: {self.active_protocol_tuple}"
1421+
rawtxs = [tx.serialize() for tx in txs]
1422+
assert all(is_hex_str(rawtx) for rawtx in rawtxs)
1423+
assert all(tx.txid() is not None for tx in txs)
1424+
timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)
1425+
for tx in txs:
1426+
if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):
1427+
raise DummyAddressUsedInTxException("tried to broadcast tx with dummy address!")
1428+
try:
1429+
res = await self.session.send_request('blockchain.transaction.broadcast_package', [rawtxs], timeout=timeout)
1430+
except aiorpcx.jsonrpc.CodeMessageError as e:
1431+
self.logger.info(f"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}. {rawtxs=}")
1432+
return False
1433+
success = assert_dict_contains_field(res, field_name='success')
1434+
if not success:
1435+
errors = assert_dict_contains_field(res, field_name='errors')
1436+
self.logger.info(f"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(errors))}. {rawtxs=}")
1437+
return False
1438+
assert success
1439+
# broadcast succeeded.
1440+
# We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting
1441+
# the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
1442+
for tx, rawtx in zip(txs, rawtxs):
1443+
self._rawtx_cache[tx.txid()] = bytes.fromhex(rawtx)
1444+
return True
1445+
14081446
async def get_history_for_scripthash(self, sh: str) -> List[dict]:
14091447
if not is_hash256_str(sh):
14101448
raise Exception(f"{repr(sh)} is not a scripthash")
@@ -1525,10 +1563,14 @@ async def get_donation_address(self) -> str:
15251563
async def get_relay_fee(self) -> int:
15261564
"""Returns the min relay feerate in sat/kbyte."""
15271565
# do request
1528-
res = await self.session.send_request('blockchain.relayfee')
1566+
if self.active_protocol_tuple >= (1, 6):
1567+
res = await self.session.send_request('mempool.get_info')
1568+
minrelaytxfee = assert_dict_contains_field(res, field_name='minrelaytxfee')
1569+
else:
1570+
minrelaytxfee = await self.session.send_request('blockchain.relayfee')
15291571
# check response
1530-
assert_non_negative_int_or_float(res)
1531-
relayfee = int(res * bitcoin.COIN)
1572+
assert_non_negative_int_or_float(minrelaytxfee)
1573+
relayfee = int(minrelaytxfee * bitcoin.COIN)
15321574
relayfee = max(0, relayfee)
15331575
return relayfee
15341576

electrum/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
ELECTRUM_VERSION = '4.6.2' # version of the client package
22

33
PROTOCOL_VERSION_MIN = '1.4' # electrum protocol
4-
PROTOCOL_VERSION_MAX = '1.4'
4+
PROTOCOL_VERSION_MAX = '1.6'
55

66
# The hash of the mnemonic seed must begin with this
77
SEED_PREFIX = '01' # Standard wallet

tests/test_interface.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,23 @@ async def handle_request(self, request):
157157
'blockchain.transaction.get': self._handle_transaction_get,
158158
'blockchain.transaction.broadcast': self._handle_transaction_broadcast,
159159
'blockchain.transaction.get_merkle': self._handle_transaction_get_merkle,
160+
'mempool.get_info': self._handle_mempool_get_info,
160161
'server.ping': self._handle_ping,
161162
}
162163
handler = handlers.get(request.method)
163164
self._method_counts[request.method] += 1
164165
coro = aiorpcx.handler_invocation(handler, request)()
165166
return await coro
166167

167-
async def _handle_server_version(self, client_name='', protocol_version=None):
168-
return ['best_server_impl/0.1', '1.4']
168+
async def _handle_server_version(self, client_name='', protocol_version=None, *args, **kwargs):
169+
return ['toy_server/0.1', '1.6']
169170

170171
async def _handle_server_features(self) -> dict:
171172
return {
172173
'genesis_hash': constants.net.GENESIS,
173174
'hosts': {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},
174-
'protocol_max': '1.7.0',
175-
'protocol_min': '1.4.3',
175+
'protocol_max': '1.6',
176+
'protocol_min': '1.6',
176177
'pruning': None,
177178
'server_version': 'ElectrumX 1.19.0',
178179
'hash_function': 'sha256',
@@ -181,6 +182,13 @@ async def _handle_server_features(self) -> dict:
181182
async def _handle_estimatefee(self, number, mode=None):
182183
return 0.00001000
183184

185+
async def _handle_mempool_get_info(self):
186+
return {
187+
"mempoolminfee": 0.00001000,
188+
"minrelaytxfee": 0.00001000,
189+
"incrementalrelayfee": 0.00001000,
190+
}
191+
184192
def _get_headersub_result(self):
185193
return {'hex': BLOCK_HEADERS[self.cur_height].hex(), 'height': self.cur_height}
186194

@@ -195,8 +203,8 @@ async def _handle_block_headers(self, start_height, count):
195203
assert start_height <= self.cur_height, (start_height, self.cur_height)
196204
last_height = min(start_height+count-1, self.cur_height) # [start_height, last_height]
197205
count = last_height - start_height + 1
198-
headers = b"".join(BLOCK_HEADERS[idx] for idx in range(start_height, last_height+1))
199-
return {'hex': headers.hex(), 'count': count, 'max': 2016}
206+
headers = list(BLOCK_HEADERS[idx].hex() for idx in range(start_height, last_height+1))
207+
return {'headers': headers, 'count': count, 'max': 2016}
200208

201209
async def _handle_ping(self):
202210
return None

0 commit comments

Comments
 (0)