Skip to content

Commit ef7c822

Browse files
committed
Expose block filters over REST.
This adds a new rest endpoint: /rest/blockfilter/filtertype/requesttype/blockhash (eg /rest/blockfilter/basic/header/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.hex) which exposes either the filter "header" or the filter data itself. Most of the code is cribbed from the equivalent RPC.
1 parent 810ce36 commit ef7c822

File tree

2 files changed

+217
-5
lines changed

2 files changed

+217
-5
lines changed

src/rest.cpp

Lines changed: 212 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
// Distributed under the MIT software license, see the accompanying
44
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

6+
#include <blockfilter.h>
67
#include <chain.h>
78
#include <chainparams.h>
89
#include <core_io.h>
910
#include <httpserver.h>
11+
#include <index/blockfilterindex.h>
1012
#include <index/txindex.h>
1113
#include <node/blockstorage.h>
1214
#include <node/context.h>
@@ -30,6 +32,7 @@
3032
#include <univalue.h>
3133

3234
static const size_t MAX_GETUTXOS_OUTPOINTS = 15; //allow a max of 15 outpoints to be queried at once
35+
static constexpr unsigned int MAX_REST_HEADERS_RESULTS = 2000;
3336

3437
enum class RetFormat {
3538
UNDEF,
@@ -190,8 +193,8 @@ static bool rest_headers(const std::any& context,
190193
return RESTERR(req, HTTP_BAD_REQUEST, "No header count specified. Use /rest/headers/<count>/<hash>.<ext>.");
191194

192195
const auto parsed_count{ToIntegral<size_t>(path[0])};
193-
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > 2000) {
194-
return RESTERR(req, HTTP_BAD_REQUEST, "Header count out of range: " + path[0]);
196+
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
197+
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, path[0]));
195198
}
196199

197200
std::string hashStr = path[1];
@@ -254,7 +257,7 @@ static bool rest_headers(const std::any& context,
254257
return true;
255258
}
256259
default: {
257-
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: .bin, .hex, .json)");
260+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
258261
}
259262
}
260263
}
@@ -337,6 +340,210 @@ static bool rest_block_notxdetails(const std::any& context, HTTPRequest* req, co
337340
return rest_block(context, req, strURIPart, false);
338341
}
339342

343+
344+
static bool rest_filter_header(const std::any& context, HTTPRequest* req, const std::string& strURIPart)
345+
{
346+
if (!CheckWarmup(req))
347+
return false;
348+
std::string param;
349+
const RetFormat rf = ParseDataFormat(param, strURIPart);
350+
351+
std::vector<std::string> uri_parts;
352+
boost::split(uri_parts, param, boost::is_any_of("/"));
353+
if (uri_parts.size() != 3) {
354+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>");
355+
}
356+
357+
uint256 block_hash;
358+
if (!ParseHashStr(uri_parts[2], block_hash)) {
359+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[2]);
360+
}
361+
362+
BlockFilterType filtertype;
363+
if (!BlockFilterTypeByName(uri_parts[0], filtertype)) {
364+
return RESTERR(req, HTTP_BAD_REQUEST, "Unknown filtertype " + uri_parts[0]);
365+
}
366+
367+
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
368+
if (!index) {
369+
return RESTERR(req, HTTP_BAD_REQUEST, "Index is not enabled for filtertype " + uri_parts[0]);
370+
}
371+
372+
const auto parsed_count{ToIntegral<size_t>(uri_parts[1])};
373+
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
374+
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, uri_parts[1]));
375+
}
376+
377+
std::vector<const CBlockIndex *> headers;
378+
headers.reserve(*parsed_count);
379+
{
380+
ChainstateManager* maybe_chainman = GetChainman(context, req);
381+
if (!maybe_chainman) return false;
382+
ChainstateManager& chainman = *maybe_chainman;
383+
LOCK(cs_main);
384+
CChain& active_chain = chainman.ActiveChain();
385+
const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(block_hash);
386+
while (pindex != nullptr && active_chain.Contains(pindex)) {
387+
headers.push_back(pindex);
388+
if (headers.size() == *parsed_count)
389+
break;
390+
pindex = active_chain.Next(pindex);
391+
}
392+
}
393+
394+
bool index_ready = index->BlockUntilSyncedToCurrentChain();
395+
396+
std::vector<uint256> filter_headers;
397+
filter_headers.reserve(*parsed_count);
398+
for (const CBlockIndex *pindex : headers) {
399+
uint256 filter_header;
400+
if (!index->LookupFilterHeader(pindex, filter_header)) {
401+
std::string errmsg = "Filter not found.";
402+
403+
if (!index_ready) {
404+
errmsg += " Block filters are still in the process of being indexed.";
405+
} else {
406+
errmsg += " This error is unexpected and indicates index corruption.";
407+
}
408+
409+
return RESTERR(req, HTTP_NOT_FOUND, errmsg);
410+
}
411+
filter_headers.push_back(filter_header);
412+
}
413+
414+
switch (rf) {
415+
case RetFormat::BINARY: {
416+
CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
417+
for (const uint256& header : filter_headers) {
418+
ssHeader << header;
419+
}
420+
421+
std::string binaryHeader = ssHeader.str();
422+
req->WriteHeader("Content-Type", "application/octet-stream");
423+
req->WriteReply(HTTP_OK, binaryHeader);
424+
return true;
425+
}
426+
case RetFormat::HEX: {
427+
CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
428+
for (const uint256& header : filter_headers) {
429+
ssHeader << header;
430+
}
431+
432+
std::string strHex = HexStr(ssHeader) + "\n";
433+
req->WriteHeader("Content-Type", "text/plain");
434+
req->WriteReply(HTTP_OK, strHex);
435+
return true;
436+
}
437+
case RetFormat::JSON: {
438+
UniValue jsonHeaders(UniValue::VARR);
439+
for (const uint256& header : filter_headers) {
440+
jsonHeaders.push_back(header.GetHex());
441+
}
442+
443+
std::string strJSON = jsonHeaders.write() + "\n";
444+
req->WriteHeader("Content-Type", "application/json");
445+
req->WriteReply(HTTP_OK, strJSON);
446+
return true;
447+
}
448+
default: {
449+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
450+
}
451+
}
452+
}
453+
454+
static bool rest_block_filter(const std::any& context, HTTPRequest* req, const std::string& strURIPart)
455+
{
456+
if (!CheckWarmup(req))
457+
return false;
458+
std::string param;
459+
const RetFormat rf = ParseDataFormat(param, strURIPart);
460+
461+
//request is sent over URI scheme /rest/blockfilter/filtertype/blockhash
462+
std::vector<std::string> uri_parts;
463+
boost::split(uri_parts, param, boost::is_any_of("/"));
464+
if (uri_parts.size() != 2) {
465+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilter/<filtertype>/<blockhash>");
466+
}
467+
468+
uint256 block_hash;
469+
if (!ParseHashStr(uri_parts[1], block_hash)) {
470+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[1]);
471+
}
472+
473+
BlockFilterType filtertype;
474+
if (!BlockFilterTypeByName(uri_parts[0], filtertype)) {
475+
return RESTERR(req, HTTP_BAD_REQUEST, "Unknown filtertype " + uri_parts[0]);
476+
}
477+
478+
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
479+
if (!index) {
480+
return RESTERR(req, HTTP_BAD_REQUEST, "Index is not enabled for filtertype " + uri_parts[0]);
481+
}
482+
483+
const CBlockIndex* block_index;
484+
bool block_was_connected;
485+
{
486+
ChainstateManager* maybe_chainman = GetChainman(context, req);
487+
if (!maybe_chainman) return false;
488+
ChainstateManager& chainman = *maybe_chainman;
489+
LOCK(cs_main);
490+
block_index = chainman.m_blockman.LookupBlockIndex(block_hash);
491+
if (!block_index) {
492+
return RESTERR(req, HTTP_NOT_FOUND, uri_parts[1] + " not found");
493+
}
494+
block_was_connected = block_index->IsValid(BLOCK_VALID_SCRIPTS);
495+
}
496+
497+
bool index_ready = index->BlockUntilSyncedToCurrentChain();
498+
499+
BlockFilter filter;
500+
if (!index->LookupFilter(block_index, filter)) {
501+
std::string errmsg = "Filter not found.";
502+
503+
if (!block_was_connected) {
504+
errmsg += " Block was not connected to active chain.";
505+
} else if (!index_ready) {
506+
errmsg += " Block filters are still in the process of being indexed.";
507+
} else {
508+
errmsg += " This error is unexpected and indicates index corruption.";
509+
}
510+
511+
return RESTERR(req, HTTP_NOT_FOUND, errmsg);
512+
}
513+
514+
switch (rf) {
515+
case RetFormat::BINARY: {
516+
CDataStream ssResp(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
517+
ssResp << filter;
518+
519+
std::string binaryResp = ssResp.str();
520+
req->WriteHeader("Content-Type", "application/octet-stream");
521+
req->WriteReply(HTTP_OK, binaryResp);
522+
return true;
523+
}
524+
case RetFormat::HEX: {
525+
CDataStream ssResp(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
526+
ssResp << filter;
527+
528+
std::string strHex = HexStr(ssResp) + "\n";
529+
req->WriteHeader("Content-Type", "text/plain");
530+
req->WriteReply(HTTP_OK, strHex);
531+
return true;
532+
}
533+
case RetFormat::JSON: {
534+
UniValue ret(UniValue::VOBJ);
535+
ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
536+
std::string strJSON = ret.write() + "\n";
537+
req->WriteHeader("Content-Type", "application/json");
538+
req->WriteReply(HTTP_OK, strJSON);
539+
return true;
540+
}
541+
default: {
542+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
543+
}
544+
}
545+
}
546+
340547
// A bit of a hack - dependency on a function defined in rpc/blockchain.cpp
341548
RPCHelpMan getblockchaininfo();
342549

@@ -717,6 +924,8 @@ static const struct {
717924
{"/rest/tx/", rest_tx},
718925
{"/rest/block/notxdetails/", rest_block_notxdetails},
719926
{"/rest/block/", rest_block_extended},
927+
{"/rest/blockfilter/", rest_block_filter},
928+
{"/rest/blockfilterheaders/", rest_filter_header},
720929
{"/rest/chaininfo", rest_chaininfo},
721930
{"/rest/mempool/info", rest_mempool_info},
722931
{"/rest/mempool/contents", rest_mempool_contents},

test/functional/interface_rest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class RESTTest (BitcoinTestFramework):
4141
def set_test_params(self):
4242
self.setup_clean_chain = True
4343
self.num_nodes = 2
44-
self.extra_args = [["-rest"], []]
44+
self.extra_args = [["-rest", "-blockfilterindex=1"], []]
4545
self.supports_cli = False
4646

4747
def skip_test_if_missing_module(self):
@@ -278,11 +278,14 @@ def run_test(self):
278278
self.sync_all()
279279
json_obj = self.test_rest_request(f"/headers/5/{bb_hash}")
280280
assert_equal(len(json_obj), 5) # now we should have 5 header objects
281+
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
282+
assert_equal(len(json_obj), 5) # now we should have 5 filter header objects
283+
self.test_rest_request(f"/blockfilter/basic/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
281284

282285
# Test number parsing
283286
for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']:
284287
assert_equal(
285-
bytes(f'Header count out of range: {num}\r\n', 'ascii'),
288+
bytes(f'Header count out of acceptable range (1-2000): {num}\r\n', 'ascii'),
286289
self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400),
287290
)
288291

0 commit comments

Comments
 (0)