Skip to content

Commit 70d6a09

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#17631: Expose block filters over REST
2b64fa3 Update REST docs with new accessors (Matt Corallo) ef7c822 Expose block filters over REST. (Matt Corallo) Pull request description: 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. You can test it at http://bitcoin-rest.bitcoin.ninja/rest//blockfilter/basic/header/000000005b7a58a939b2636f61fa4ddd62258c5fed57667a35d23f2334c4f86d.hex ACKs for top commit: dergoegge: ACK 2b64fa3 - Adding blockfilters to the REST interface is analogous to serving other public data such as transactions or blocks. Tree-SHA512: d487bc694266375c94d6fcf2e9d788a8a42a3b94e8d3290e46335a64cbcde55084ce5ea6119b79a4065888d94d7c3ae25a59a901fa46e3711f0eb296add12696
2 parents 8eeb4e9 + 2b64fa3 commit 70d6a09

File tree

3 files changed

+231
-5
lines changed

3 files changed

+231
-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: 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>
@@ -31,6 +33,7 @@
3133
#include <univalue.h>
3234

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

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

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

198201
std::string hashStr = path[1];
@@ -255,7 +258,7 @@ static bool rest_headers(const std::any& context,
255258
return true;
256259
}
257260
default: {
258-
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: .bin, .hex, .json)");
261+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: " + AvailableDataFormatsString() + ")");
259262
}
260263
}
261264
}
@@ -338,6 +341,210 @@ static bool rest_block_notxdetails(const std::any& context, HTTPRequest* req, co
338341
return rest_block(context, req, strURIPart, TxVerbosity::SHOW_TXID);
339342
}
340343

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

@@ -718,6 +925,8 @@ static const struct {
718925
{"/rest/tx/", rest_tx},
719926
{"/rest/block/notxdetails/", rest_block_notxdetails},
720927
{"/rest/block/", rest_block_extended},
928+
{"/rest/blockfilter/", rest_block_filter},
929+
{"/rest/blockfilterheaders/", rest_filter_header},
721930
{"/rest/chaininfo", rest_chaininfo},
722931
{"/rest/mempool/info", rest_mempool_info},
723932
{"/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):
@@ -272,11 +272,14 @@ def run_test(self):
272272
self.generate(self.nodes[1], 5)
273273
json_obj = self.test_rest_request(f"/headers/5/{bb_hash}")
274274
assert_equal(len(json_obj), 5) # now we should have 5 header objects
275+
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
276+
assert_equal(len(json_obj), 5) # now we should have 5 filter header objects
277+
self.test_rest_request(f"/blockfilter/basic/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
275278

276279
# Test number parsing
277280
for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']:
278281
assert_equal(
279-
bytes(f'Header count out of range: {num}\r\n', 'ascii'),
282+
bytes(f'Header count out of acceptable range (1-2000): {num}\r\n', 'ascii'),
280283
self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400),
281284
)
282285

0 commit comments

Comments
 (0)