feat(minibf): add the /pools/{pool_id}/blocks endpoint - #1365
slowbackspace wants to merge 6 commits into
Conversation
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`
|
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 configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (9)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughChangesThe archive now indexes blocks by issuer pool hash through a new Pool-minted 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
Merge Risk: ⚪ Minimal · up to 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)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Some tools did not complete. Review the errors below. 🔧 ast-grep (0.45.3)crates/minibf/src/lib.rsast-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. Comment |
There was a problem hiding this comment.
🟡 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_blocksarchive 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.
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.
There was a problem hiding this comment.
🔵 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_idonly checksstarts_with("pool1"), ignores the decoded HRP, and uses genericbech32::decode, which also accepts Bech32m (the distinction is documented inmapping.rs:239-246). Thus a 28-byte known operator encoded with HRPpool1x, or with a Bech32m checksum, reaches the index and returns 200 instead of the required malformed-ID 400. Validate the exactpoolHRP 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
|
Re the second Copilot pass ("pool identifiers with an invalid HRP or Bech32m checksum can be accepted"): confirmed, but pre-existing. |
The cardano crate does not know about Blockfrost. Describe the dimension and the tagging in ledger terms only.
Closes #1080.
Summary
GET /pools/{pool_id}/blocksreturns 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)order=descis the exact reverse[][]pool1…that decodes to a payload other than 28 bytesInvalid or malformed pool id format.pool1…nor 56 hex chars, bad checksum, bad hexInvalid or malformed pool id format.order,pageorcountout of rangefrom/to, well-formed or malformedOne accepted divergence from db-sync, negligible. Blockfrost 404s a pool id with no
pool_hashrow. 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'sPoolStateset.slot_leaderrow 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.
pool_blocksis the thirteenth archive dimension. The literal is storage format, because the stores hash it into on-disk keys.index_blocktags every block withHasher::<224>of its header issuer vkey. That is the derivation the minted-block counter already uses, so the tag key equals thePoolStateentity key.by_id_blockspages 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 pluscountbody reads, so a deep page costs what the first page costs.PoolStateand no tagged block, the same rule the epoch-scoped sibling uses. A pool must register before it can mint, soPoolStatecovers every pool db-sync would have.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
pool16h8ugt8k0a4kxa5g6x062zjrgfjc7cehpw0ze8374axlul76932has 100,557 blocks. Latency is flat across page depth.count=100)page=1page=500page=1000page=2000(past the end)order=desc,page=1Index size (measured on the same store). The
archive-tagskeyspace holds one 24-byte key per tagged block and no value.pool_blocksadds 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:
count=100)page=1page=100page=500page=1000(past the end)Testing
cargo test -p dolos-cardano --all-targets: 271 passed, 0 failed.cargo test -p dolos-minibf --lib routes::pools: 68 passed, 0 failed.cargo test -p dolos-snapshot: 181 passed, 0 failed, 14 ignored; the goldens reproduce the new values on a re-run.cargo test --workspace --all-targets: 1568 passed, 0 failed, 33 ignored.cargo clippy --all-targets --all-features -- -D warningsclean.cargo +nightly-2026-08-27 fmt --all --checkclean.yarn test:preview -t "pool_id/blocks|…/blocks": 11 passed, 0 failed (best pool via bech32 and hex,count=2&page=2slice, 7 pagination-error fixtures).blocks_mintedfrom/pools/{id}equals the number of hashes paged out of/pools/{id}/blocksfor 26 pools (2.2M blocks, no duplicate hash across pages,descis the reverse ofasc). 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 twoblocks_mintedreadings. The 30 newest hashes of that pool all resolve through/blocks/{hash}with it asslot_leader.yarn 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 ofpool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdyand itscount=2&page=2slice, requested by both bech32 and hex id.blocks_mintedis 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.access-filters, and it is unrelated. It expects 403Endpoint is not allowed.from Blockfrost's per-project endpoint permissions, which dolos does not implement; the same fixture fails on every endpoint,/,genesisandblocks/100included.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
GET /pools/{id}/blocksendpoint, returning block hashes in chronological order with pagination support.Documentation