Skip to content

feat(minibf): add the /pools/{pool_id}/blocks endpoint - #1365

Open
slowbackspace wants to merge 6 commits into
mainfrom
feat/minibf-pool-blocks
Open

slowbackspace wants to merge 6 commits into
mainfrom
feat/minibf-pool-blocks

Conversation

@slowbackspace

@slowbackspace slowbackspace commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Closes #1080.

Summary

GET /pools/{pool_id}/blocks returns the hashes of the blocks a pool minted, in chain order, paginated. A new full-chain archive dimension maps the pool operator hash to the slots of its blocks, so the endpoint no longer has to scan and hash every block of a range.

Semantics (pinned against ryo pools_pool_id_blocks.sql)

Input Response
Registered pool, bech32 or 56-char hex id 200, block hashes oldest first; order=desc is the exact reverse
Registered pool that minted nothing 200 []
Page past the end 200 []
pool1… that decodes to a payload other than 28 bytes 400 Invalid or malformed pool id format.
Neither pool1… nor 56 hex chars, bad checksum, bad hex 400 Invalid or malformed pool id format.
Well-formed pool id never seen on chain 404
order, page or count out of range 400, shared pagination messages
from / to, well-formed or malformed ignored; the Blockfrost spec does not declare them for this route
Byron / EBB blocks never listed; they carry no issuer vkey

One accepted divergence from db-sync, negligible. Blockfrost 404s a pool id with no pool_hash row. db-sync creates that row on pool registration, and the ledger has rejected delegation to an unregistered pool since Shelley, so the row set equals dolos's PoolState set.

  • Genesis-delegate issuers — blocks minted by Shelley genesis delegates have a slot_leader row with no pool. Blockfrost 404s their key hash, dolos lists their blocks.

Implementation

The index change is one dimension; the endpoint is a slot scan plus one body read per listed block.

  • Dimensionpool_blocks is the thirteenth archive dimension. The literal is storage format, because the stores hash it into on-disk keys.
  • Taggingindex_block tags every block with Hasher::<224> of its header issuer vkey. That is the derivation the minted-block counter already uses, so the tag key equals the PoolState entity key.
  • Handlerby_id_blocks pages over the slot iterator first and reads only the bodies of the page. There is no scan budget: the cost is one key-only prefix scan plus count body reads, so a deep page costs what the first page costs.
  • 404 rule — a 404 needs both no PoolState and no tagged block, the same rule the epoch-scoped sibling uses. A pool must register before it can mint, so PoolState covers every pool db-sync would have.
  • Goldens re-pin — the canonical indexes layer carries one record per dimension, so its diffId, its record count (16 → 17), its size (431 → 456 bytes) and the inscription digest move deliberately.
  • Existing stores — a store synced before this change has no tags for its old blocks, and a re-bootstrap is the migration.
  • Snapshot profile — the profile text calls a dimension-set change a media-type-version event, but the version is one global constant for all 20 kinds. The bump is left to the maintainers' call; see the review thread. Until then an old reader fails closed on a new stele with UnknownDimension("pool_blocks").

Out of scope on purpose: the sibling /epochs/{epoch}/blocks/{pool_id} keeps its own range scan.

Performance (measured, preview, fresh Mithril bootstrap with the PR binary, M-series laptop)

The preview best pool pool16h8ugt8k0a4kxa5g6x062zjrgfjc7cehpw0ze8374axlul76932 has 100,557 blocks. Latency is flat across page depth.

Request (count=100) Latency
page=1 14 ms
page=500 18 ms
page=1000 12 ms
page=2000 (past the end) 11 ms
order=desc, page=1 18 ms

Index size (measured on the same store). The archive-tags keyspace holds one 24-byte key per tagged block and no value. pool_blocks adds 4,673,938 entries under 501 keys on preview, 3.9% of all tag entries, about 54 MB at the keyspace's average 11.6 bytes per entry on disk. Mainnet has 9.47M Shelley-era blocks today, so the estimate there is about 110 MB.

Mainnet, on a full archive (13.94M blocks, 9,450,712 tagged), same flat profile:

Request (count=100) Latency
page=1 2.3 ms
page=100 0.9 ms
page=500 0.8 ms
page=1000 (past the end) 0.8 ms

Testing

  • dolos-cardano — 2 new unit tests (issuer tagged, Byron untagged) plus the updated dimension pin and producer-coverage tests; cargo test -p dolos-cardano --all-targets: 271 passed, 0 failed.
  • dolos-minibf — 10 new inline tests (happy path, hex id, pagination, desc, empty pool, ignored from/to, 400 id, 400 pagination, 404, 500); cargo test -p dolos-minibf --lib routes::pools: 68 passed, 0 failed.
  • dolos-snapshotcargo test -p dolos-snapshot: 181 passed, 0 failed, 14 ignored; the goldens reproduce the new values on a re-run.
  • Workspacecargo test --workspace --all-targets: 1568 passed, 0 failed, 33 ignored. cargo clippy --all-targets --all-features -- -D warnings clean. cargo +nightly-2026-08-27 fmt --all --check clean.
  • blockfrost-tests, preview — fresh preview bootstrap with the PR binary (52 min import). yarn test:preview -t "pool_id/blocks|…/blocks": 11 passed, 0 failed (best pool via bech32 and hex, count=2&page=2 slice, 7 pagination-error fixtures).
  • Cross-check, previewblocks_minted from /pools/{id} equals the number of hashes paged out of /pools/{id}/blocks for 26 pools (2.2M blocks, no duplicate hash across pages, desc is the reverse of asc). The three deltas seen while the daemon was still catching up vanished once it reached the tip; the most active pool (288,074 blocks, 2,881 pages) matched exactly when bracketed by two blocks_minted readings. The 30 newest hashes of that pool all resolve through /blocks/{hash} with it as slot_leader.
  • blockfrost-tests, mainnetyarn test:mainnet -t "pool_id/blocks": 4 passed, 0 failed; the route's pagination-error fixtures add 5 more passes. The recorded fixture hashes match exactly: the first 100 blocks of pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy and its count=2&page=2 slice, requested by both bech32 and hex id.
  • Cross-check, mainnetblocks_minted is 3,787 for that pool and paging the endpoint yields exactly 3,787 unique hashes over 38 pages. Byron is covered here in a way preview cannot cover it: the archive holds 4,490,686 Byron blocks and none of them is tagged.
  • One mainnet fixture fails, access-filters, and it is unrelated. It expects 403 Endpoint is not allowed. from Blockfrost's per-project endpoint permissions, which dolos does not implement; the same fixture fails on every endpoint, /, genesis and blocks/100 included.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for identifying blocks produced by their pool.
    • Added the GET /pools/{id}/blocks endpoint, returning block hashes in chronological order with pagination support.
    • Added pool block indexing for efficient archive lookups.
    • Invalid pool IDs are rejected, and missing or unsupported blocks are handled safely.
  • Documentation

    • Documented the new pool blocks endpoint in the Mini Blockfrost API coverage.

Only a per-epoch count of minted blocks exists today, so listing the
blocks a pool minted means scanning every block of a range and hashing
each issuer key. Tag the block with its issuer pool at index time
instead.

- `index_block` is the single shared path, so the roll batch, the
  catchup and the undo all inherit the tag; a rollback removes exactly
  the tag the apply wrote
- the tag key is `Hasher::<224>` of the header issuer vkey, the same
  derivation `visit_root` uses for the minted-block counter, so the key
  equals the `PoolState` entity key
- Byron blocks carry no issuer vkey and stay untagged
- `slots_by_pool_blocks` reads the dimension back as a slot iterator
- stores synced before this commit carry no tags for their old blocks
The canonical indexes layer carries one record per archive dimension,
so pool_blocks moves its diffId, its record count and the inscription
digest on top of it.

- indexes layer: 17 records, 456 bytes uncompressed, diffId
  sha256:4ce2748a3559145097b9cee18d384b23684b03879efc199ff16f5a9b9d19644b
- inscription digest:
  sha256:767497d283078a27ae447cb51c9c0080fc71069a993d0f0c88529c324d9cf6d9
- the dimension count in the coverage test and the prose in the goldens
  module documentation both go from twelve to thirteen
Return the block hashes a pool minted, in chain order, paginated. The
handler reads the pool_blocks archive dimension, so it pages over slots
first and reads only the bodies of the page.

- accepts both the bech32 and the hex form of the pool id, and rejects
  a bech32 payload that is not 28 bytes with a 400, as the sibling
  updates endpoint does
- `from`/`to` are not in the Blockfrost spec for this route, so they are
  ignored rather than honored
- no scan budget: the cost is one key-only prefix scan plus `count` body
  reads, so a deep page costs what the first page costs
- 404 only when the pool has neither a `PoolState` nor a tagged block,
  the same rule the epoch-scoped sibling uses
- `decode_block_header` becomes `pub(crate)` and is called across the
  module boundary, as `epochs` already calls `pools::decode_pool_id`
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5a686393-2a8e-4ae5-8d0f-918ff2b0fd8d

📥 Commits

Reviewing files that changed from the base of the PR and between ce3d042 and 857eff0.

📒 Files selected for processing (9)
  • crates/cardano/src/indexes/delta.rs
  • crates/cardano/src/indexes/dimensions.rs
  • crates/cardano/src/indexes/ext.rs
  • crates/minibf/src/lib.rs
  • crates/minibf/src/routes/epochs/mod.rs
  • crates/minibf/src/routes/pools.rs
  • crates/snapshot/tests/coverage.rs
  • crates/snapshot/tests/goldens.rs
  • docs/content/apis/minibf.mdx

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Changes

The archive now indexes blocks by issuer pool hash through a new pool_blocks dimension. Mini Blockfrost adds GET /pools/{id}/blocks, with pagination, ordering, validation, and block-hash responses. Tests, snapshot goldens, and API documentation are updated.

Pool-minted blocks

Layer / File(s) Summary
Issuer pool indexing
crates/cardano/src/indexes/delta.rs, crates/cardano/src/indexes/dimensions.rs, crates/cardano/src/indexes/ext.rs, crates/snapshot/tests/*
The archive adds the pool_blocks dimension. Non-Byron blocks with an issuer key receive a hashed pool tag. Slot lookup and dimension snapshots include the new index.
Pool blocks endpoint
crates/minibf/src/routes/pools.rs, crates/minibf/src/routes/epochs/mod.rs, crates/minibf/src/lib.rs
The router registers GET /pools/{id}/blocks. The handler validates identifiers, pages indexed slots, reads block headers, skips missing and Byron blocks, and returns ordered hashes.
API documentation
docs/content/apis/minibf.mdx
The supported endpoint list includes /pools/{id}/blocks.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant by_id_blocks
  participant CardanoArchiveIndexExt
  participant ArchiveStore
  Client->>by_id_blocks: Request pool block hashes
  by_id_blocks->>CardanoArchiveIndexExt: Query pool_blocks by pool and slot range
  CardanoArchiveIndexExt->>ArchiveStore: Read indexed slots and block bodies
  ArchiveStore-->>by_id_blocks: Slots and decoded headers
  by_id_blocks-->>Client: Ordered block hashes
Loading

Merge Risk: ⚪ Minimal · up to 857ef

The added endpoint retains the tagged main block at EBB boundary slots, so no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the /pools/{pool_id}/blocks endpoint to minibf.
Linked Issues check ✅ Passed The PR implements the coding requirements in issue #1080. crates/minibf/src/lib.rs registers GET /pools/{id}/blocks, and crates/minibf/src/routes/pools.rs implements pool ID decoding, 400 valida…
Out of Scope Changes check ✅ Passed The changed files support issue #1080. The archive dimension and issuer tags provide the required data source. Snapshot updates maintain expected archive goldens after the new dimension. Visibility ch…
Docstring Coverage ✅ Passed Docstring coverage is 87.88% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 8 files. (1 skipped: 1 …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Warning

Some tools did not complete. Review the errors below.

🔧 ast-grep (0.45.3)
crates/minibf/src/lib.rs

ast-grep timed out on this file


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The endpoint mishandles ignored range parameters, and the snapshot dimension change lacks the required media-type/profile version bump.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds Blockfrost-compatible pool block listing backed by a new archive index.

Changes:

  • Adds the pool_blocks archive dimension and issuer tagging.
  • Implements and tests /pools/{id}/blocks.
  • Updates documentation and snapshot goldens.
File summaries
File Description
docs/content/apis/minibf.mdx Documents the endpoint.
crates/snapshot/tests/goldens.rs Updates snapshot golden values.
crates/snapshot/tests/coverage.rs Updates dimension-count coverage.
crates/minibf/src/routes/pools.rs Implements and tests pool block listing.
crates/minibf/src/routes/epochs/mod.rs Exposes header decoding internally.
crates/minibf/src/lib.rs Registers the route.
crates/cardano/src/indexes/ext.rs Adds the pool-block lookup helper.
crates/cardano/src/indexes/dimensions.rs Defines the new archive dimension.
crates/cardano/src/indexes/delta.rs Indexes block issuer pool hashes.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/cardano/src/indexes/dimensions.rs
Comment thread crates/minibf/src/routes/pools.rs
The Blockfrost spec does not declare `from`/`to` for this route, but the
shared pagination parser still rejected a malformed or reversed window
with a 400. Clear both fields before validation, as the account
addresses and asset txs routes already do.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Pool identifiers with an invalid HRP or Bech32m checksum can be accepted instead of returning 400.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/minibf/src/routes/pools.rs:1120

  • This route still accepts non-pool Bech32 encodings. decode_pool_id only checks starts_with("pool1"), ignores the decoded HRP, and uses generic bech32::decode, which also accepts Bech32m (the distinction is documented in mapping.rs:239-246). Thus a 28-byte known operator encoded with HRP pool1x, or with a Bech32m checksum, reaches the index and returns 200 instead of the required malformed-ID 400. Validate the exact pool HRP and the Bech32 checksum type in the shared pool-ID decoder, and add these cases to the new endpoint tests.
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@slowbackspace

Copy link
Copy Markdown
Contributor Author

Re the second Copilot pass ("pool identifiers with an invalid HRP or Bech32m checksum can be accepted"): confirmed, but pre-existing. decode_pool_id is the shared decoder for all nine pool routes since #637, and the old /pools/{id} route accepts the same ids on the current main lineage. This PR only adds one more caller. Tracked in #1366, so the fix lands once for every route.

The cardano crate does not know about Blockfrost. Describe the
dimension and the tagging in ledger terms only.
@slowbackspace
slowbackspace marked this pull request as ready for review September 22, 2026 11:37
@slowbackspace
slowbackspace requested review from a team and scarmuega as code owners September 22, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

minibf: add /pools/{pool_id}/blocks

2 participants