Blockbook is Trezor's blockchain indexer and block explorer backend. It sits in
front of a Bitcoin Core node and exposes a REST + WebSocket API (/api/v2/…).
Like Esplora and block-dn it has no getchaintips-like endpoint, so we would
only get its single active tip - useful alongside at least one Bitcoin Core or
btcd node on the same network.
What the API gives us
Verified against a public mainnet instance (https://bitcoin.atomicwallet.io,
Blockbook devel / build 2026-07-29, backend /Satoshi:31.1.0/). The
*.trezor.io instances are behind Cloudflare and block non-browser clients, so
they can't be used for testing (or as a configured backend) without a browser
user agent.
Node trait method |
Blockbook endpoint |
tips() |
GET /api/v2 → blockbook.bestHeight + backend.bestBlockHash |
version() |
GET /api/v2 → blockbook.version and/or backend.subversion |
block_hash(height) |
GET /api/v2/block-index/<height> → {"blockHash": "…"} |
block_header_hash(hash) |
GET /api/v2/block/<hash> (header fields, see below) |
coinbase(hash) |
GET /api/v2/block/<hash> → txs[0].txid, then GET /api/v2/tx-specific/<txid> → hex |
block(hash) |
GET /api/v2/rawblock/<hash> → {"hex": "…"} |
batch_header_fetch() |
not available |
Headers have to be rebuilt from the block JSON
Blockbook has no header endpoint, but /api/v2/block/<hash|height> returns all
six header fields (api.BlockInfo): version (JSON number), previousBlockHash,
merkleRoot, time, bits (compact hex), nonce (decimal string). Rebuilding
the 80-byte header from those and checking that it hashes to the requested hash
works - checked field by field against the first 80 bytes of
/api/v2/rawblock/962050:
JSON: version 537608192, prev …4815eda1b, merkle 1af428e7…, time 1786478836,
bits "1702353d", nonce "500239977"
raw: 00400b20 1bda5e81… 60048642… f4807b6a 3d350217 690ed11d ✓
That is the same pattern MempoolSpace already uses (header_from_mempool_space_block
verifies the header hashes to the hash we asked for), except mempool.space hands
out the serialized header directly and here we assemble it.
The catch: a header costs a page of transactions
/api/v2/block/<hash> always ships up to 1000 full transactions with the header
fields; page is the only knob (pageSize is fixed at 1000 server-side), and
asking for a page past the end clamps to the last one instead of returning an
empty tx list. Measured on block 962050 (3167 txs): 1.1 MB for page=1, 161 kB
for the last page (167 txs). So the cheapest header fetch is
?page=<large> at roughly 1 kB per transaction on the last page - on average a
few hundred kB per header. (Blockbook does serve gzip - 40 kB for that 161 kB
response - but minreq doesn't decompress, so we'd pay the full size.)
That rules out backfilling and makes this a tip-following backend, same
reasoning as mempool.space: batch_header_fetch: false, run it next to a
Bitcoin Core node that supplies the active chain so new_active_headers stops
at the first header it already knows, and keep min_fork_height close to the
tip. Worth spelling out in the README like the mempool.space note does.
Other findings
- Blockbook only indexes the main chain:
/api/v2/block/<stale hash> answers
400 {"error":"Block not found"}, so no fork branches, no stale blocks from
block(). partial_nonactive_headers: true is harmless insurance but never
kicks in, since our own tips() only reports the active tip.
/api/v2/block-index/<height above tip> answers 200 {"blockHash":""} rather
than an error - needs an explicit empty check.
/api/v2/rawtx/<txid> is not on every deployment (the instance above falls
through to the status handler), while /api/v2/tx-specific/<txid> (bitcoind's
getrawtransaction verbose output, including hex) is there and portable.
Prefer the latter for the coinbase.
- Pool identification works: we can get the raw coinbase, unlike Electrum.
- Optional follow-up: Blockbook's WebSocket (
/websocket) has
subscribeNewBlock, which would fit wait_for_tip_change() and avoid polling.
Needs a WebSocket dependency, so probably not in a first version.
Sketch
src/backend/blockbook.rs with a Blockbook struct (info, api_url),
HeaderFetchType::Hash, batch_header_fetch: false
Backend::Blockbook in src/config.rs ("blockbook"), FetchError::BlockbookREST
rpc_host = "https://<host>/api/v2", following the Esplora style
- README section +
config.toml.example entry, incl. the backfill limitation
Blockbook is Trezor's blockchain indexer and block explorer backend. It sits in
front of a Bitcoin Core node and exposes a REST + WebSocket API (
/api/v2/…).Like Esplora and block-dn it has no
getchaintips-like endpoint, so we wouldonly get its single active tip - useful alongside at least one Bitcoin Core or
btcd node on the same network.
What the API gives us
Verified against a public mainnet instance (
https://bitcoin.atomicwallet.io,Blockbook
devel/ build 2026-07-29, backend/Satoshi:31.1.0/). The*.trezor.ioinstances are behind Cloudflare and block non-browser clients, sothey can't be used for testing (or as a configured backend) without a browser
user agent.
Nodetrait methodtips()GET /api/v2→blockbook.bestHeight+backend.bestBlockHashversion()GET /api/v2→blockbook.versionand/orbackend.subversionblock_hash(height)GET /api/v2/block-index/<height>→{"blockHash": "…"}block_header_hash(hash)GET /api/v2/block/<hash>(header fields, see below)coinbase(hash)GET /api/v2/block/<hash>→txs[0].txid, thenGET /api/v2/tx-specific/<txid>→hexblock(hash)GET /api/v2/rawblock/<hash>→{"hex": "…"}batch_header_fetch()Headers have to be rebuilt from the block JSON
Blockbook has no header endpoint, but
/api/v2/block/<hash|height>returns allsix header fields (
api.BlockInfo):version(JSON number),previousBlockHash,merkleRoot,time,bits(compact hex),nonce(decimal string). Rebuildingthe 80-byte header from those and checking that it hashes to the requested hash
works - checked field by field against the first 80 bytes of
/api/v2/rawblock/962050:That is the same pattern
MempoolSpacealready uses (header_from_mempool_space_blockverifies the header hashes to the hash we asked for), except mempool.space hands
out the serialized header directly and here we assemble it.
The catch: a header costs a page of transactions
/api/v2/block/<hash>always ships up to 1000 full transactions with the headerfields;
pageis the only knob (pageSizeis fixed at 1000 server-side), andasking for a page past the end clamps to the last one instead of returning an
empty tx list. Measured on block 962050 (3167 txs): 1.1 MB for
page=1, 161 kBfor the last page (167 txs). So the cheapest header fetch is
?page=<large>at roughly 1 kB per transaction on the last page - on average afew hundred kB per header. (Blockbook does serve gzip - 40 kB for that 161 kB
response - but minreq doesn't decompress, so we'd pay the full size.)
That rules out backfilling and makes this a tip-following backend, same
reasoning as mempool.space:
batch_header_fetch: false, run it next to aBitcoin Core node that supplies the active chain so
new_active_headersstopsat the first header it already knows, and keep
min_fork_heightclose to thetip. Worth spelling out in the README like the mempool.space note does.
Other findings
/api/v2/block/<stale hash>answers400 {"error":"Block not found"}, so no fork branches, no stale blocks fromblock().partial_nonactive_headers: trueis harmless insurance but neverkicks in, since our own
tips()only reports the active tip./api/v2/block-index/<height above tip>answers200 {"blockHash":""}ratherthan an error - needs an explicit empty check.
/api/v2/rawtx/<txid>is not on every deployment (the instance above fallsthrough to the status handler), while
/api/v2/tx-specific/<txid>(bitcoind'sgetrawtransactionverbose output, includinghex) is there and portable.Prefer the latter for the coinbase.
/websocket) hassubscribeNewBlock, which would fitwait_for_tip_change()and avoid polling.Needs a WebSocket dependency, so probably not in a first version.
Sketch
src/backend/blockbook.rswith aBlockbookstruct (info,api_url),HeaderFetchType::Hash,batch_header_fetch: falseBackend::Blockbookinsrc/config.rs("blockbook"),FetchError::BlockbookRESTrpc_host = "https://<host>/api/v2", following the Esplora styleconfig.toml.exampleentry, incl. the backfill limitation