Skip to content

Commit d4e212e

Browse files
committed
rest: fetch spent transaction outputs by blockhash
Today, it is possible to fetch a block's spent prevouts in order to build an external index by using the `/rest/block/HASH.json` endpoint. However, its performance is low due to JSON serialization overhead. We can significantly optimize it by adding a new REST endpoint, using a binary response format: ``` $ 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/spentoutputs/$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: * https://github.com/romanz/bindex-rs This PR would allow to improve the performance of external indexers such as electrs, ElectrumX, Fulcrum and Blockbook: * https://github.com/romanz/electrs (also https://github.com/Blockstream/electrs and https://github.com/mempool/electrs) * https://github.com/spesmilo/electrumx * https://github.com/cculianu/Fulcrum * https://github.com/trezor/blockbook
1 parent c461d15 commit d4e212e

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

src/rest.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <streams.h>
2828
#include <sync.h>
2929
#include <txmempool.h>
30+
#include <undo.h>
3031
#include <util/any.h>
3132
#include <util/check.h>
3233
#include <util/strencodings.h>
@@ -281,6 +282,113 @@ static bool rest_headers(const std::any& context,
281282
}
282283
}
283284

285+
/**
286+
* Serialize spent outputs as a list of per-transaction CTxOut lists using binary format.
287+
*/
288+
static void SerializeBlockUndo(DataStream& stream, const CBlockUndo& block_undo)
289+
{
290+
WriteCompactSize(stream, block_undo.vtxundo.size() + 1);
291+
WriteCompactSize(stream, 0); // block_undo.vtxundo doesn't contain coinbase tx
292+
for (const CTxUndo& tx_undo : block_undo.vtxundo) {
293+
WriteCompactSize(stream, tx_undo.vprevout.size());
294+
for (const Coin& coin : tx_undo.vprevout) {
295+
coin.out.Serialize(stream);
296+
}
297+
}
298+
}
299+
300+
/**
301+
* Serialize spent outputs as a list of per-transaction CTxOut lists using JSON format.
302+
*/
303+
static void BlockUndoToJSON(const CBlockUndo& block_undo, UniValue& result)
304+
{
305+
result.push_back({UniValue::VARR}); // block_undo.vtxundo doesn't contain coinbase tx
306+
for (const CTxUndo& tx_undo : block_undo.vtxundo) {
307+
UniValue tx_prevouts(UniValue::VARR);
308+
for (const Coin& coin : tx_undo.vprevout) {
309+
UniValue prevout(UniValue::VOBJ);
310+
prevout.pushKV("value", ValueFromAmount(coin.out.nValue));
311+
312+
UniValue script_pub_key(UniValue::VOBJ);
313+
ScriptToUniv(coin.out.scriptPubKey, /*out=*/script_pub_key, /*include_hex=*/true, /*include_address=*/true);
314+
prevout.pushKV("scriptPubKey", std::move(script_pub_key));
315+
316+
tx_prevouts.push_back(std::move(prevout));
317+
}
318+
result.push_back(std::move(tx_prevouts));
319+
}
320+
}
321+
322+
static bool rest_spent_txouts(const std::any& context, HTTPRequest* req, const std::string& strURIPart)
323+
{
324+
if (!CheckWarmup(req)) {
325+
return false;
326+
}
327+
std::string param;
328+
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
329+
std::vector<std::string> path = SplitString(param, '/');
330+
331+
std::string hashStr;
332+
if (path.size() == 1) {
333+
// path with query parameter: /rest/spenttxouts/<hash>
334+
hashStr = path[0];
335+
} else {
336+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/spenttxouts/<hash>.<ext>");
337+
}
338+
339+
auto hash{uint256::FromHex(hashStr)};
340+
if (!hash) {
341+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
342+
}
343+
344+
ChainstateManager* chainman = GetChainman(context, req);
345+
if (!chainman) {
346+
return false;
347+
}
348+
349+
const CBlockIndex* pblockindex = WITH_LOCK(cs_main, return chainman->m_blockman.LookupBlockIndex(*hash));
350+
if (!pblockindex) {
351+
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
352+
}
353+
354+
CBlockUndo block_undo;
355+
if (pblockindex->nHeight > 0 && !chainman->m_blockman.ReadBlockUndo(block_undo, *pblockindex)) {
356+
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " undo not available");
357+
}
358+
359+
switch (rf) {
360+
case RESTResponseFormat::BINARY: {
361+
DataStream ssSpentResponse{};
362+
SerializeBlockUndo(ssSpentResponse, block_undo);
363+
req->WriteHeader("Content-Type", "application/octet-stream");
364+
req->WriteReply(HTTP_OK, ssSpentResponse);
365+
return true;
366+
}
367+
368+
case RESTResponseFormat::HEX: {
369+
DataStream ssSpentResponse{};
370+
SerializeBlockUndo(ssSpentResponse, block_undo);
371+
const std::string strHex{HexStr(ssSpentResponse) + "\n"};
372+
req->WriteHeader("Content-Type", "text/plain");
373+
req->WriteReply(HTTP_OK, strHex);
374+
return true;
375+
}
376+
377+
case RESTResponseFormat::JSON: {
378+
UniValue result(UniValue::VARR);
379+
BlockUndoToJSON(block_undo, result);
380+
std::string strJSON = result.write() + "\n";
381+
req->WriteHeader("Content-Type", "application/json");
382+
req->WriteReply(HTTP_OK, strJSON);
383+
return true;
384+
}
385+
386+
default: {
387+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
388+
}
389+
}
390+
}
391+
284392
static bool rest_block(const std::any& context,
285393
HTTPRequest* req,
286394
const std::string& strURIPart,
@@ -1021,6 +1129,7 @@ static const struct {
10211129
{"/rest/deploymentinfo/", rest_deploymentinfo},
10221130
{"/rest/deploymentinfo", rest_deploymentinfo},
10231131
{"/rest/blockhashbyheight/", rest_blockhash_by_height},
1132+
{"/rest/spenttxouts/", rest_spent_txouts},
10241133
};
10251134

10261135
void StartREST(const std::any& context)

test/functional/interface_rest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from decimal import Decimal
88
from enum import Enum
9+
from io import BytesIO
910
import http.client
1011
import json
1112
import typing
@@ -15,6 +16,7 @@
1516
from test_framework.messages import (
1617
BLOCK_HEADER_SIZE,
1718
COIN,
19+
deser_block_spent_outputs,
1820
)
1921
from test_framework.test_framework import BitcoinTestFramework
2022
from test_framework.util import (
@@ -424,6 +426,34 @@ def run_test(self):
424426
assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}"))
425427
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}"))
426428

429+
self.log.info("Test the /spenttxouts URI")
430+
431+
block_count = self.nodes[0].getblockcount()
432+
for height in range(0, block_count + 1):
433+
blockhash = self.nodes[0].getblockhash(height)
434+
spent_bin = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.BIN, ret_type=RetType.BYTES)
435+
spent_hex = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.HEX, ret_type=RetType.BYTES)
436+
spent_json = self.test_rest_request(f"/spenttxouts/{blockhash}", req_type=ReqType.JSON, ret_type=RetType.JSON)
437+
438+
assert_equal(bytes.fromhex(spent_hex.decode()), spent_bin)
439+
440+
spent = deser_block_spent_outputs(BytesIO(spent_bin))
441+
block = self.nodes[0].getblock(blockhash, 3) # return prevout for each input
442+
assert_equal(len(spent), len(block["tx"]))
443+
assert_equal(len(spent_json), len(block["tx"]))
444+
445+
for i, tx in enumerate(block["tx"]):
446+
prevouts = [txin["prevout"] for txin in tx["vin"] if "coinbase" not in txin]
447+
# compare with `getblock` JSON output (coinbase tx has no prevouts)
448+
actual = [(txout.scriptPubKey.hex(), Decimal(txout.nValue) / COIN) for txout in spent[i]]
449+
expected = [(p["scriptPubKey"]["hex"], p["value"]) for p in prevouts]
450+
assert_equal(expected, actual)
451+
# also compare JSON format
452+
actual = [(prevout["scriptPubKey"], prevout["value"]) for prevout in spent_json[i]]
453+
expected = [(p["scriptPubKey"], p["value"]) for p in prevouts]
454+
assert_equal(expected, actual)
455+
456+
427457
self.log.info("Test the /deploymentinfo URI")
428458

429459
deployment_info = self.nodes[0].getdeploymentinfo()

test/functional/test_framework/messages.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ def ser_string_vector(l):
228228
return r
229229

230230

231+
def deser_block_spent_outputs(f):
232+
nit = deser_compact_size(f)
233+
return [deser_vector(f, CTxOut) for _ in range(nit)]
234+
235+
231236
def from_hex(obj, hex_string):
232237
"""Deserialize from a hex string representation (e.g. from RPC)
233238

0 commit comments

Comments
 (0)