Skip to content

Commit 8840250

Browse files
committed
"scripthash.get_history": handle client_statushash and client_height
1 parent 9de63be commit 8840250

File tree

2 files changed

+62
-20
lines changed

2 files changed

+62
-20
lines changed

electrumx/server/history.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections import defaultdict
1414
from typing import TYPE_CHECKING, Type, Optional, Dict, Sequence, Tuple, List
1515
import itertools
16+
from functools import partial
1617

1718
from aiorpcx import run_in_thread
1819

@@ -323,6 +324,7 @@ def get_txnums(
323324
it.seek(prefix + pack_txnum(txnum_min))
324325
txnum_min = txnum_min if txnum_min is not None else 0
325326
txnum_max = txnum_max if txnum_max is not None else float('inf')
327+
assert txnum_min <= txnum_max, f"txnum_min={txnum_min}, txnum_max={txnum_max}"
326328
for db_key, db_val in it:
327329
tx_numb = db_key[-TXNUM_LEN:]
328330
if limit == 0:
@@ -359,29 +361,46 @@ def get_spender_txnum_for_txo(self, prev_txnum: int, txout_idx: int) -> Optional
359361
spender_txnum = unpack_txnum(spender_txnumb)
360362
return spender_txnum
361363

362-
def fs_get_intermediate_statushash_for_hashx(self, hashX: bytes) -> Tuple[int, bytes]:
364+
def fs_get_intermediate_statushash_for_hashx(
365+
self,
366+
*,
367+
hashX: bytes,
368+
txnum_max: int = None,
369+
) -> Tuple[int, bytes]:
363370
'''For a hashX, returns (tx_num, status), with the latest stored statushash
364-
and corresponding tx_num.
371+
and corresponding tx_num, where tx_num < txnum_max.
365372
This can be used to efficiently calculate the status of a hashX as
366373
only the txs mined after(>) tx_num will need to be hashed.
367374
'''
375+
# first, search in-memory, among the unflushed statuses
368376
unflushed_statushashes = self._unflushed_hashx_to_statushash.get(hashX, [])
369377
if len(unflushed_statushashes) > 0:
370-
tx_num, status = unflushed_statushashes[-1]
378+
for tx_num, status in reversed(unflushed_statushashes):
379+
if txnum_max is None or tx_num < txnum_max:
380+
return tx_num, status
381+
# second, search in the on-disk DB
382+
prefix = b'S' + hashX
383+
it = self.db.iterator(prefix=prefix, reverse=True)
384+
if txnum_max is not None:
385+
it.seek(prefix + pack_txnum(txnum_max))
386+
for db_key, db_val in it:
387+
tx_numb = db_key[-TXNUM_LEN:]
388+
tx_num = unpack_txnum(tx_numb)
389+
status = db_val
390+
break
371391
else:
372-
prefix = b'S' + hashX
373-
for db_key, db_val in self.db.iterator(prefix=prefix, reverse=True):
374-
tx_numb = db_key[-TXNUM_LEN:]
375-
tx_num = unpack_txnum(tx_numb)
376-
status = db_val
377-
break
378-
else:
379-
tx_num = -1
380-
status = bytes(32)
392+
tx_num = 0
393+
status = bytes(32)
381394
return tx_num, status
382395

383-
async def get_intermediate_statushash_for_hashx(self, hashX: bytes) -> Tuple[int, bytes]:
384-
return await run_in_thread(self.fs_get_intermediate_statushash_for_hashx, hashX)
396+
async def get_intermediate_statushash_for_hashx(
397+
self,
398+
*,
399+
hashX: bytes,
400+
txnum_max: int = None,
401+
) -> Tuple[int, bytes]:
402+
f = partial(self.fs_get_intermediate_statushash_for_hashx, hashX=hashX, txnum_max=txnum_max)
403+
return await run_in_thread(f)
385404

386405
def store_intermediate_statushash_for_hashx(
387406
self,
@@ -397,6 +416,7 @@ def store_intermediate_statushash_for_hashx(
397416
'''
398417
if hashX not in self._unflushed_hashx_to_statushash:
399418
self._unflushed_hashx_to_statushash[hashX] = []
419+
# maintain invariant that unflushed statuses are in order (increasing tx_num):
400420
if len(self._unflushed_hashx_to_statushash[hashX]) > 0:
401421
tx_num_last, status_last = self._unflushed_hashx_to_statushash[hashX][-1]
402422
if tx_num <= tx_num_last:

electrumx/server/session.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,12 +1218,19 @@ async def _address_status_proto_legacy(self, hashX: bytes) -> Optional[str]:
12181218

12191219
return status
12201220

1221-
async def _address_status_proto_1_5(self, hashX: bytes) -> str:
1222-
'''Returns an address status, as per protocol newer than >=1.5'''
1223-
# first, consider confirmed history
1224-
tx_num_calced, status = await self.db.history.get_intermediate_statushash_for_hashx(hashX)
1221+
async def _calc_intermediate_status_for_hashX(
1222+
self,
1223+
*,
1224+
hashX: bytes,
1225+
txnum_max: int = None,
1226+
) -> bytes:
1227+
'''Returns the status of a hashX, considering only confirmed history
1228+
up to (<) txnum_max.
1229+
'''
1230+
tx_num_calced, status = await self.db.history.get_intermediate_statushash_for_hashx(
1231+
hashX=hashX, txnum_max=txnum_max)
12251232
db_history = await self.db.limited_history_triples(
1226-
hashX=hashX, limit=None, txnum_min=tx_num_calced+1)
1233+
hashX=hashX, limit=None, txnum_min=tx_num_calced+1, txnum_max=txnum_max)
12271234
self.bump_cost(0.3 + len(db_history) * 0.001) # cost of history-lookup
12281235
self.bump_cost(36 * len(db_history) * 0.00002) # cost of hashing mined txs
12291236
reorgsafe_height = self.db.db_height - self.env.reorg_limit
@@ -1234,6 +1241,12 @@ async def _address_status_proto_1_5(self, hashX: bytes) -> str:
12341241
if cnt % storestatus_period == 0 and height < reorgsafe_height:
12351242
self.db.history.store_intermediate_statushash_for_hashx(
12361243
hashX=hashX, tx_num=tx_num, status=status)
1244+
return status
1245+
1246+
async def _address_status_proto_1_5(self, hashX: bytes) -> str:
1247+
'''Returns an address status, as per protocol newer than >=1.5'''
1248+
# first, consider confirmed history
1249+
status = await self._calc_intermediate_status_for_hashX(hashX=hashX)
12371250

12381251
# second, consider mempool txs
12391252
mempool = await self.mempool.transaction_summaries(hashX)
@@ -1389,7 +1402,16 @@ async def scripthash_get_history_proto_1_5(
13891402
raise RPCError(BAD_REQUEST, f'from_height={from_height} '
13901403
f'<= client_height={client_height} '
13911404
f'< to_height={to_height} must hold.')
1392-
# TODO implement handling. they are ignored for now
1405+
# Done sanitising args; start handling
1406+
# Check if client status is consistent with server; if so we can fast-forward from_height
1407+
if client_statushash is not None:
1408+
client_txnum = self.db.get_next_tx_num_after_blockheight(client_height)
1409+
server_statushash = await self._calc_intermediate_status_for_hashX(
1410+
hashX=hashX,
1411+
txnum_max=client_txnum + 1,
1412+
)
1413+
if server_statushash == client_statushash:
1414+
from_height = client_height + 1
13931415

13941416
# Limit size of returned history to ensure it fits within a response.
13951417
limit_bytes = self.env.max_send - HISTORY_OVER_WIRE_OVERHEAD_BYTES

0 commit comments

Comments
 (0)