Skip to content

Commit 427d07f

Browse files
committed
merge bitcoin#17631: Expose block filters over REST
1 parent d60f15e commit 427d07f

File tree

3 files changed

+229
-5
lines changed

3 files changed

+229
-5
lines changed

doc/REST-interface.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ With the /notxdetails/ option JSON response will only contain the transaction ha
5252
Given a block hash: returns <COUNT> amount of blockheaders in upward direction.
5353
Returns empty if the block doesn't exist or it isn't in the active chain.
5454

55+
#### Blockfilter Headers
56+
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
57+
58+
Given a block hash: returns <COUNT> amount of blockfilter headers in upward
59+
direction for the filter type <FILTERTYPE>.
60+
Returns empty if the block doesn't exist or it isn't in the active chain.
61+
62+
#### Blockfilters
63+
`GET /rest/blockfilter/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>`
64+
65+
Given a block hash: returns the block filter of the given block of type
66+
<FILTERTYPE>.
67+
Responds with 404 if the block doesn't exist.
68+
5569
#### Blockhash by height
5670
`GET /rest/blockhashbyheight/<HEIGHT>.<bin|hex|json>`
5771

src/rest.cpp

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
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 <context.h>
910
#include <core_io.h>
1011
#include <httpserver.h>
12+
#include <index/blockfilterindex.h>
1113
#include <index/txindex.h>
1214
#include <llmq/chainlocks.h>
1315
#include <llmq/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,
@@ -189,8 +192,8 @@ static bool rest_headers(const CoreContext& context,
189192
return RESTERR(req, HTTP_BAD_REQUEST, "No header count specified. Use /rest/headers/<count>/<hash>.<ext>.");
190193

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

196199
std::string hashStr = path[1];
@@ -253,7 +256,7 @@ static bool rest_headers(const CoreContext& context,
253256
return true;
254257
}
255258
default: {
256-
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: .bin, .hex, .json)");
259+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
257260
}
258261
}
259262
}
@@ -336,6 +339,208 @@ static bool rest_block_notxdetails(const CoreContext& context, HTTPRequest* req,
336339
return rest_block(context, req, strURIPart, false);
337340
}
338341

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

@@ -716,6 +921,8 @@ static const struct {
716921
{"/rest/tx/", rest_tx},
717922
{"/rest/block/notxdetails/", rest_block_notxdetails},
718923
{"/rest/block/", rest_block_extended},
924+
{"/rest/blockfilter/", rest_block_filter},
925+
{"/rest/blockfilterheaders/", rest_filter_header},
719926
{"/rest/chaininfo", rest_chaininfo},
720927
{"/rest/mempool/info", rest_mempool_info},
721928
{"/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
@@ -45,7 +45,7 @@ class RESTTest (BitcoinTestFramework):
4545
def set_test_params(self):
4646
self.setup_clean_chain = True
4747
self.num_nodes = 2
48-
self.extra_args = [["-rest"], []]
48+
self.extra_args = [["-rest", "-blockfilterindex=1"], []]
4949
self.supports_cli = False
5050

5151
def skip_test_if_missing_module(self):
@@ -282,11 +282,14 @@ def run_test(self):
282282
self.generate(self.nodes[1], 5)
283283
json_obj = self.test_rest_request("/headers/5/{}".format(bb_hash))
284284
assert_equal(len(json_obj), 5) # now we should have 5 header objects
285+
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
286+
assert_equal(len(json_obj), 5) # now we should have 5 filter header objects
287+
self.test_rest_request(f"/blockfilter/basic/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
285288

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

0 commit comments

Comments
 (0)