Merge bitcoin/bitcoin#32540: rest: fetch spent transaction outputs by blockhash

c48846ec41 doc: add release notes for #32540 (Roman Zeyde)
d4e212e8a6 rest: fetch spent transaction outputs by blockhash (Roman Zeyde)

Pull request description:

  Today, it is possible to fetch a block's spent prevouts in order to build an external index by using the `/rest/block/BLOCKHASH.json` endpoint. However, its performance is low due to JSON serialization overhead.

  We can significantly optimize it by adding a new [REST API](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md) endpoint, using a binary response format (returning a collection of spent txout lists, one per each block transaction):

  ```
  $ BLOCKHASH=00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054

  $ ab -k -c 1 -n 100 http://localhost:8332/rest/block/$BLOCKHASH.json
  Document Length:        13278152 bytes
  Requests per second:    3.53 [#/sec] (mean)
  Time per request:       283.569 [ms] (mean)

  $ ab -k -c 1 -n 10000 http://localhost:8332/rest/spenttxouts/$BLOCKHASH.bin
  Document Length:        195591 bytes
  Requests per second:    254.47 [#/sec] (mean)
  Time per request:       3.930 [ms] (mean)
  ```

  Currently, this PR is being used and tested by Bindex[^1].

  This PR would allow to improve the performance of external indexers such as electrs[^2], ElectrumX[^3], Fulcrum[^4] and Blockbook[^5].

  [^1]: https://github.com/romanz/bindex-rs
  [^2]: https://github.com/romanz/electrs (also [blockstream.info](https://github.com/Blockstream/electrs) and [mempool.space](https://github.com/mempool/electrs) forks)
  [^3]: https://github.com/spesmilo/electrumx
  [^4]: https://github.com/cculianu/Fulcrum
  [^5]: https://github.com/trezor/blockbook

ACKs for top commit:
  maflcko:
    re-ACK c48846ec41 📶
  TheCharlatan:
    Re-ACK c48846ec41
  achow101:
    ACK c48846ec41

Tree-SHA512: cf423541be90d6615289760494ae849b7239b69427036db6cc528ac81df10900f514471d81a460125522c5ffa31e9747ddfca187a1f93151e4ae77fe773c6b7b
This commit is contained in:
Ava Chow
2025-06-27 14:44:41 -07:00
4 changed files with 149 additions and 0 deletions

View File

@@ -6,6 +6,7 @@
from decimal import Decimal
from enum import Enum
from io import BytesIO
import http.client
import json
import typing
@@ -15,6 +16,7 @@ import urllib.parse
from test_framework.messages import (
BLOCK_HEADER_SIZE,
COIN,
deser_block_spent_outputs,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -426,6 +428,34 @@ class RESTTest (BitcoinTestFramework):
assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}"))
assert_equal(self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}"))
self.log.info("Test the /spenttxouts URI")
block_count = self.nodes[0].getblockcount()
for height in range(0, block_count + 1):
blockhash = self.nodes[0].getblockhash(height)
spent_bin = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.BIN, ret_type=RetType.BYTES)
spent_hex = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.HEX, ret_type=RetType.BYTES)
spent_json = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.JSON, ret_type=RetType.JSON)
assert_equal(bytes.fromhex(spent_hex.decode()), spent_bin)
spent = deser_block_spent_outputs(BytesIO(spent_bin))
block = self.nodes[0].getblock(blockhash, 3) # return prevout for each input
assert_equal(len(spent), len(block["tx"]))
assert_equal(len(spent_json), len(block["tx"]))
for i, tx in enumerate(block["tx"]):
prevouts = [txin["prevout"] for txin in tx["vin"] if "coinbase" not in txin]
# compare with `getblock` JSON output (coinbase tx has no prevouts)
actual = [(txout.scriptPubKey.hex(), Decimal(txout.nValue) / COIN) for txout in spent[i]]
expected = [(p["scriptPubKey"]["hex"], p["value"]) for p in prevouts]
assert_equal(expected, actual)
# also compare JSON format
actual = [(prevout["scriptPubKey"], prevout["value"]) for prevout in spent_json[i]]
expected = [(p["scriptPubKey"], p["value"]) for p in prevouts]
assert_equal(expected, actual)
self.log.info("Test the /deploymentinfo URI")
deployment_info = self.nodes[0].getdeploymentinfo()