Merge bitcoin/bitcoin#33657: rest: allow reading partial block data from storage
07135290c1rest: allow reading partial block data from storage (Roman Zeyde)4e2af1c065blockstorage: allow reading partial block data from storage (Roman Zeyde)f2fd1aa21cblockstorage: return an error code from `ReadRawBlock()` (Roman Zeyde) Pull request description: It allows fetching specific transactions using an external index, following https://github.com/bitcoin/bitcoin/pull/32541#issuecomment-3267485313. Currently, electrs and other indexers map between an address/scripthash to the list of the relevant transactions. However, in order to fetch those transactions from bitcoind, electrs relies on reading the whole block and post-filtering for a specific transaction[^1]. Other indexers use a `txindex` to fetch a transaction using its txid [^2][^3][^4]. The above approach has significant storage and CPU overhead, since the `txid` is a pseudo-random 32-byte value. Also, mainnet `txindex` takes ~60GB today. This PR is adding support for using the transaction's position within its block to be able to fetch it directly using [REST API](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md), using the following HTTP request: ``` GET /rest/blockpart/BLOCKHASH.bin?offset=OFFSET&size=SIZE ``` - The offsets' index can be encoded much more efficiently ([~1.3GB today](https://github.com/romanz/bindex-rs/pull/66#issuecomment-3508476436)). - Address history query performance can be tested on mainnet using [1BitcoinEaterAddressDontSendf59kuE](https://mempool.space/address/1BitcoinEaterAddressDontSendf59kuE) - assuming warm OS block cache, [it takes <1s to fetch 5200 txs, i.e. <0.2ms per tx](https://github.com/romanz/bindex-rs/pull/66#issuecomment-3508476436) with [bindex](https://github.com/romanz/bindex-rs). - Only binary and hex response formats are supported. [^1]: https://github.com/romanz/electrs/blob/master/doc/schema.md [^2]: https://github.com/Blockstream/electrs/blob/new-index/doc/schema.md#txstore [^3]: https://github.com/spesmilo/electrumx/blob/master/docs/HOWTO.rst#prerequisites [^4]: https://github.com/cculianu/Fulcrum/blob/master/README.md#requirements ACKs for top commit: maflcko: review ACK07135290c1🏪 l0rinc: ACK07135290c1hodlinator: re-ACK07135290c1Tree-SHA512: bcce7bf4b9a3e5e920ab5a83e656f50d5d7840cdde6b7147d329cf578f8a2db555fc1aa5334e8ee64d5630d25839ece77a2cf421c6c3ac1fa379bb453163bd4f
This commit is contained in:
11 files changed
+216
-48
No files matched your search
@@ -28,7 +28,6 @@ from test_framework.wallet import (
|
||||
MiniWallet,
|
||||
getnewdestination,
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
|
||||
INVALID_PARAM = "abc"
|
||||
@@ -66,13 +65,16 @@ class RESTTest (BitcoinTestFramework):
|
||||
body: str = '',
|
||||
status: int = 200,
|
||||
ret_type: RetType = RetType.JSON,
|
||||
query_params: Optional[dict[str, typing.Any]] = None,
|
||||
query_params: typing.Union[dict[str, typing.Any], str, None] = None,
|
||||
) -> typing.Union[http.client.HTTPResponse, bytes, str, None]:
|
||||
rest_uri = '/rest' + uri
|
||||
if req_type in ReqType:
|
||||
rest_uri += f'.{req_type.name.lower()}'
|
||||
if query_params:
|
||||
rest_uri += f'?{urllib.parse.urlencode(query_params)}'
|
||||
if isinstance(query_params, str):
|
||||
rest_uri += f'?{query_params}'
|
||||
else:
|
||||
rest_uri += f'?{urllib.parse.urlencode(query_params)}'
|
||||
|
||||
conn = http.client.HTTPConnection(self.url.hostname, self.url.port)
|
||||
self.log.debug(f'{http_method} {rest_uri} {body}')
|
||||
@@ -82,7 +84,7 @@ class RESTTest (BitcoinTestFramework):
|
||||
conn.request('POST', rest_uri, body)
|
||||
resp = conn.getresponse()
|
||||
|
||||
assert_equal(resp.status, status)
|
||||
assert resp.status == status, f"Expected: {status}, Got: {resp.status} - Response: {str(resp.read())}"
|
||||
|
||||
if ret_type == RetType.OBJ:
|
||||
return resp
|
||||
@@ -455,6 +457,52 @@ class RESTTest (BitcoinTestFramework):
|
||||
expected = [(p["scriptPubKey"], p["value"]) for p in prevouts]
|
||||
assert_equal(expected, actual)
|
||||
|
||||
self.log.info("Test the /blockpart URI")
|
||||
|
||||
blockhash = self.nodes[0].getbestblockhash()
|
||||
block_bin = self.test_rest_request(f"/block/{blockhash}", req_type=ReqType.BIN, ret_type=RetType.BYTES)
|
||||
for req_type in (ReqType.BIN, ReqType.HEX):
|
||||
def get_block_part(status: int = 200, **kwargs):
|
||||
resp = self.test_rest_request(f"/blockpart/{blockhash}", status=status,
|
||||
req_type=req_type, ret_type=RetType.BYTES, **kwargs)
|
||||
assert isinstance(resp, bytes)
|
||||
if req_type is ReqType.HEX and status == 200:
|
||||
resp = bytes.fromhex(resp.decode().strip())
|
||||
return resp
|
||||
|
||||
assert_equal(block_bin, get_block_part(query_params={"offset": 0, "size": len(block_bin)}))
|
||||
|
||||
assert len(block_bin) >= 500
|
||||
assert_equal(block_bin[20:320], get_block_part(query_params={"offset": 20, "size": 300}))
|
||||
assert_equal(block_bin[-5:], get_block_part(query_params={"offset": len(block_bin) - 5, "size": 5}))
|
||||
|
||||
get_block_part(status=400, query_params={"offset": 10})
|
||||
get_block_part(status=400, query_params={"size": 100})
|
||||
get_block_part(status=400, query_params={"offset": "x"})
|
||||
get_block_part(status=400, query_params={"size": "y"})
|
||||
get_block_part(status=400, query_params={"offset": "x", "size": "y"})
|
||||
assert get_block_part(status=400, query_params="%XY").decode("utf-8").startswith("URI parsing failed")
|
||||
|
||||
get_block_part(status=400, query_params={"offset": 0, "size": 0})
|
||||
get_block_part(status=400, query_params={"offset": len(block_bin), "size": 0})
|
||||
get_block_part(status=400, query_params={"offset": len(block_bin) + 1, "size": 1})
|
||||
get_block_part(status=400, query_params={"offset": len(block_bin), "size": 1})
|
||||
get_block_part(status=400, query_params={"offset": len(block_bin) + 1, "size": 1})
|
||||
get_block_part(status=400, query_params={"offset": 0, "size": len(block_bin) + 1})
|
||||
|
||||
self.test_rest_request(f"/blockpart/{blockhash}", status=400, req_type=ReqType.JSON, ret_type=RetType.OBJ)
|
||||
|
||||
self.log.info("Missing block data should cause REST API to fail")
|
||||
|
||||
self.test_rest_request(f"/block/{blockhash}", status=200, req_type=ReqType.BIN, ret_type=RetType.OBJ)
|
||||
self.test_rest_request(f"/blockpart/{blockhash}", query_params={"offset": 0, "size": 1}, status=200, req_type=ReqType.BIN, ret_type=RetType.OBJ)
|
||||
blk_files = list(self.nodes[0].blocks_path.glob("blk*.dat"))
|
||||
for blk_file in blk_files:
|
||||
blk_file.rename(blk_file.with_suffix('.bkp'))
|
||||
self.test_rest_request(f"/block/{blockhash}", status=500, req_type=ReqType.BIN, ret_type=RetType.OBJ)
|
||||
self.test_rest_request(f"/blockpart/{blockhash}", query_params={"offset": 0, "size": 1}, status=500, req_type=ReqType.BIN, ret_type=RetType.OBJ)
|
||||
for blk_file in blk_files:
|
||||
blk_file.with_suffix('.bkp').rename(blk_file)
|
||||
|
||||
self.log.info("Test the /deploymentinfo URI")
|
||||
|
||||
|
||||
Reference in new issue
Block a user