Skip to content

fix(grpc): populate block_ref in AnyUtxoData via block_by_tx_hash#1000

Open
islamaliev wants to merge 2 commits into
txpipe:mainfrom
islamaliev:fix/u5c-block-ref-populate
Open

fix(grpc): populate block_ref in AnyUtxoData via block_by_tx_hash#1000
islamaliev wants to merge 2 commits into
txpipe:mainfrom
islamaliev:fix/u5c-block-ref-populate

Conversation

@islamaliev
Copy link
Copy Markdown

@islamaliev islamaliev commented May 22, 2026

What

into_u5c_utxo (in both src/serve/grpc/v1alpha/query.rs and src/serve/grpc/v1beta/query.rs) now populates AnyUtxoData.block_ref instead of returning None. Every UTxO returned by read_utxos and search_utxos will now carry the block (slot/hash/height/timestamp) that produced it.

Why

The block_ref field was added to AnyUtxoData as part of the u5c 0.18.1 upgrade in #813 (commit 2b6de0bc), but it landed as a placeholder — block_ref: None — and the reverse lookup was never wired up. Consumers querying UTxOs through read_utxos / search_utxos therefore lose all block-level metadata for those UTxOs.

A downstream library (pogun-chainfollower) shipped a filter_map tourniquet to silently drop these items so the rest of the pipeline doesn't fall over. Populating the field at the source makes that workaround unnecessary.

How

Mirror the pattern already used by read_tx (a few functions down in the same file): call AsyncQueryFacade::block_by_tx_hash(txo.0.to_vec()), decode the block with MultiEraBlock::decode, and build a ChainPoint { slot, hash, height, timestamp }. The reverse lookup is backed by the existing slot_by_tx_hash index — no new indexing is needed; it is already implemented across both storage backends (dolos-redb3, dolos-fjall).

Lookup failures (tx not in archive, decode error, query error) warn! with the tx hash and fall back to block_ref: None. This mirrors the existing posture in into_u5c_utxo for datum-decode errors — graceful degradation rather than failing the whole call.

Applied to both v1alpha and v1beta (near-identical files).

Performance tradeoff (please weigh in)

into_u5c_utxo is invoked once per UTxO. With this patch, a search_utxos page of N UTxOs costs N reverse-lookups + N full MultiEraBlock::decode calls. When the UTxOs cluster on M < N source txs (typical — multi-output txs), N − M of those decodes are redundant.

This PR takes the Tier 1 path: per-UTxO lookup, with a TODO comment near the new code marking the spot for future batching. Correctness over performance, acceptable cost for typical query sizes.

Tier 2 (deferred): refactor read_utxos / search_utxos to gather unique tx hashes from the page first, batch-fetch a HashMap<TxHash, ChainPoint>, and thread it into into_u5c_utxo via a signature change. Bigger diff, better cost profile.

Happy to land Tier 2 inline before merge if reviewers prefer it, or to track it as a follow-up issue — deferring to maintainer judgment.

Tests

No existing test harness exercises read_utxos / search_utxos against fixture blocks — the in-file test module only covers genesis/era-summary RPCs against the in-memory ToyDomain, which doesn't have meaningful archive integration for block_by_tx_hash to return Some. I didn't want to invent a heavy fixture/seeded-chain scaffolding without first asking whether maintainers want one. Please advise:

  • comfortable merging on review-only and tracking a test in a follow-up?
  • or would you like a test (and is there a preferred harness pattern — e2e under tests/ against a seeded snapshot, or a unit test with a richer mock domain)?

Quality gates (locally, against a5dc18f)

  • cargo fmt --check — the two touched files are clean. (Pre-existing fmt diffs in unrelated files exist on main because rustfmt.toml enables wrap_comments = true, which only applies on nightly. Untouched.)
  • cargo clippy --all-targets --all-features -- -D warnings — clean.
  • cargo build --workspace --all-targets --all-features — clean.
  • cargo test --workspace --all-targets — 540 passed, 0 failed (matches CI shape — ci.yml does not pass --all-features).
    • Note: cargo test --workspace --all-features shows 14 pre-existing failures in dolos-cardano proptests (model::accounts::prop_tests::*, model::epochs::prop_tests::*, model::pools::prop_tests::pool_registration_roundtrip, etc.). I verified these fail on bare main@a5dc18f without my changes, so they're unrelated to this PR.

Scope

  • Both v1alpha and v1beta updated.
  • No proto changes (block_ref: Option<ChainPoint> already exists on AnyUtxoData).
  • No new storage indexes.
  • No unrelated refactors.

Summary by CodeRabbit

  • New Features
    • UTXO query responses now include block reference metadata (slot, block hash, block height, transaction index) for returned UTXOs.
    • The system prefetches and deduplicates block lookups to reduce redundant work when resolving UTXO origins.
  • Bug Fixes
    • Missing or malformed block data is handled gracefully with warnings; queries continue when some block refs are unavailable.

Review Change Stack

`into_u5c_utxo` returned `block_ref: None` for every UTxO surfaced by
`read_utxos` / `search_utxos`, leaving downstream consumers unable to
associate UTxOs with the blocks that created them. The field was added
as a placeholder in txpipe#813 (u5c 0.18.1 upgrade) but the reverse lookup
was never wired up.

Mirror the pattern already used by `read_tx`: call
`AsyncQueryFacade::block_by_tx_hash`, decode the block, and build a
`ChainPoint { slot, hash, height, timestamp }`. Lookup failures
(missing tx in archive, decode error) `warn!` with the tx hash and
fall back to `None` — matching the existing posture for datum-decode
errors in the same function.

Applied to both v1alpha and v1beta. No new indexing is needed;
`slot_by_tx_hash` already exists across redb3 and fjall.
@islamaliev islamaliev requested a review from scarmuega as a code owner May 22, 2026 15:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

Both gRPC v1alpha and v1beta query endpoints now populate the block_ref field in UTXO responses by looking up blocks via transaction hash, decoding block data, and constructing ChainPoint metadata with slot, block hash, height, and timestamp information.

Changes

Block reference population in UTXO responses

Layer / File(s) Summary
Core block-meta API and lookup
crates/core/src/async_query.rs
Adds BlockRefMeta and AsyncQueryFacade::block_meta_by_tx_hash that finds the block and transaction index for a tx hash, decoding blocks under the blocking limiter and mapping decode failures into DomainError::ChainError.
gRPC block_refs helper module and wiring
src/serve/grpc/block_refs.rs, src/serve/grpc/mod.rs
Introduces BlockRefData and fetch_block_refs which deduplicates tx hashes, loads era summary, calls block_meta_by_tx_hash, and returns a TxHash→BlockRefData map; mod.rs registers the new module.
v1alpha: map block refs into AnyUtxoData
src/serve/grpc/v1alpha/query.rs
Adds to_chain_point, threads an optional block_ref: Option<u5c::query::ChainPoint> into into_u5c_utxo, and in read_utxos/search_utxos prefetches block refs, maps them per-UTxO, and stores them in AnyUtxoData.block_ref.
v1beta: map block refs into AnyUtxoData
src/serve/grpc/v1beta/query.rs
Mirrors v1alpha changes: adds to_chain_point, expands into_u5c_utxo to accept block_ref, and prefetches/matches block refs for UTXO responses in read_utxos/search_utxos.
sequenceDiagram
  participant Client
  participant GRPC_Handler as gRPC Handler (v1alpha/v1beta)
  participant BlockRefs as serve::grpc::block_refs::fetch_block_refs
  participant AsyncQuery as AsyncQueryFacade::block_meta_by_tx_hash
  participant Archive as Archive/Index
  Client->>GRPC_Handler: request read_utxos/search_utxos
  GRPC_Handler->>BlockRefs: fetch_block_refs(&domain, utxo_keys)
  BlockRefs->>AsyncQuery: block_meta_by_tx_hash(tx_hash)
  AsyncQuery->>Archive: load & decode block bytes
  Archive-->>AsyncQuery: block bytes / metadata
  AsyncQuery-->>BlockRefs: BlockRefMeta (slot, hash, height, tx_index)
  BlockRefs-->>GRPC_Handler: Map<TxHash, BlockRefData>
  GRPC_Handler->>GRPC_Handler: to_chain_point + into_u5c_utxo(with block_ref)
  GRPC_Handler-->>Client: UTXO response with AnyUtxoData.block_ref
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • scarmuega

Poem

🐰 A UTXO hops through blocks so grand,
Now finding where each coin once land,
With slot and hash in tidy rows,
The block_ref field proudly shows,
Where transactions bloom and chains expand! ✨

🚥 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 PR title clearly and specifically describes the main change: populating block_ref in AnyUtxoData by using block lookup via transaction hash, which aligns with the core objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/serve/grpc/v1alpha/query.rs (1)

799-799: 💤 Low value

Consider logging when tx hash is not found in archive.

The Ok(None) case (tx not found) silently falls back to block_ref: None, while other failure paths log warnings. The PR description indicates "missing tx in archive" should also log a warning for consistency.

💡 Optional: Add warning for missing tx
-        Ok(None) => None,
+        Ok(None) => {
+            warn!(
+                tx_hash = hex::encode(txo.0),
+                "block_by_tx_hash returned None for UTxO"
+            );
+            None
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/serve/grpc/v1alpha/query.rs` at line 799, The match arm currently returns
Ok(None) => None silently, leaving block_ref: None without a log; update this
branch to emit a warning when the tx hash is not found in the archive (i.e.,
when Ok(None) is matched) using the same logger/macro used by the other failure
paths in this file so it matches existing logs, then return None as before —
locate the match arm showing "Ok(None) => None" and add a warn call referencing
the missing tx/hash context before returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/serve/grpc/v1alpha/query.rs`:
- Line 799: The match arm currently returns Ok(None) => None silently, leaving
block_ref: None without a log; update this branch to emit a warning when the tx
hash is not found in the archive (i.e., when Ok(None) is matched) using the same
logger/macro used by the other failure paths in this file so it matches existing
logs, then return None as before — locate the match arm showing "Ok(None) =>
None" and add a warn call referencing the missing tx/hash context before
returning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad2af15f-803c-4619-8535-f87c49dcd2f3

📥 Commits

Reviewing files that changed from the base of the PR and between a5dc18f and 2f438fb.

📒 Files selected for processing (2)
  • src/serve/grpc/v1alpha/query.rs
  • src/serve/grpc/v1beta/query.rs

…e decode

Addresses review feedback on the per-UTxO block_ref population added in the
previous commit:

- Introduce `AsyncQueryFacade::block_meta_by_tx_hash` returning chain-point
  metadata (`BlockRefMeta`) decoded once inside the blocking task. Eliminates
  the redundant `MultiEraBlock::decode` previously performed in the gRPC
  caller. (review A-01)

- Hoist `load_era_summary` to the request handler scope via a new shared
  `serve::grpc::block_refs` module. Previously `get_slot_timestamp` reloaded
  the era summary from state on every UTxO; now it runs once per request.
  (review A-02)

- Deduplicate block lookups by source tx hash. A `search_utxos` page whose
  UTxOs cluster on M unique txs now does M lookups instead of N. (review A-03)

- Propagate storage failures as `Status::internal` instead of swallowing
  them. Block-decode failures (indicating archive corruption) degrade to
  `debug!` rather than per-UTxO `warn!` log spam. (review A-04, A-05)

- Extract the entire flow into `serve::grpc::block_refs::fetch_block_refs`,
  shared by both v1alpha and v1beta. Each version maps the chain-agnostic
  `BlockRefData` to its own proto-specific `ChainPoint` via a tiny
  `to_chain_point` helper. (review A-06)

`into_u5c_utxo` now takes the pre-resolved `Option<ChainPoint>` as a
parameter rather than performing its own lookup, keeping the per-UTxO path
cheap.

Tests deferred: no harness exists for `read_utxos`/`search_utxos`. The
`minibf::TestDomainBuilder` pattern can seed a `ToyDomain` via
`import_blocks`, and `FaultyToyDomain` covers error branches — happy to add
unit tests if maintainers prefer them in-PR.
@islamaliev
Copy link
Copy Markdown
Author

Pushed a follow-up commit (b3b1495) addressing the bulk of the review feedback on the original Tier-1 patch:

  • A-01 (double decode) — added AsyncQueryFacade::block_meta_by_tx_hash returning a chain-point-only BlockRefMeta decoded once inside the blocking task. The gRPC caller no longer re-decodes the same block.
  • A-02 (era-summary reload)load_era_summary is now called once per request, not once per UTxO. Threaded through BlockRefData::timestamp.
  • A-03 (dedupe per page) — Tier 2 batching is in. The new serve::grpc::block_refs::fetch_block_refs collects unique source tx hashes from the UTxO page, performs one lookup per unique tx, and returns a HashMap<TxHash, BlockRefData> that callers index per-item. A search_utxos page whose 100 UTxOs cluster on 20 source txs now does 20 lookups, not 100.
  • A-04 (silent error swallowing) — storage errors from block_meta_by_tx_hash now propagate as Status::internal. Ok(None) (tx not in archive) stays silent as before — that's still a legitimate state for not-yet-indexed tips and pruned/custom UTxOs.
  • A-05 (log amplification) — per-UTxO warn! is gone; block-decode failures (indicating archive corruption) drop to debug! instead.
  • A-06 (v1alpha/v1beta duplication) — the whole flow now lives in src/serve/grpc/block_refs.rs. Each version-specific query.rs only ships a 7-line to_chain_point(&BlockRefData) -> u5c::query::ChainPoint helper to convert into its own proto type.
  • C-01 (nested match) — fixed as a side effect of the helper extraction; the gRPC query.rs files no longer carry the lookup logic at all.
  • C-02 (allocation pattern) — withdrawn after verifying TxHash = Hash<32> is Copy; the original code was idiomatic.

The two into_u5c_utxo functions now just receive Option<ChainPoint> as a parameter — no per-UTxO lookup, no per-UTxO storage hits, no per-UTxO warns.

Local quality gates against the new HEAD:

  • cargo clippy --all-targets --all-features -- -D warnings — clean.
  • cargo build --workspace --all-targets --all-features — clean.
  • cargo test --workspace --all-targets — 540 passed, 0 failed (matches CI shape).
  • Touched files are cargo fmt --check-clean.

Tests still deferred per the original PR question. The reviewer noted that minibf::TestDomainBuilder (seeding ToyDomain via import_blocks) plus FaultyToyDomain for error branches would cover the happy path + archive-miss + storage-error cases in ~30–40 lines. Happy to add them in this PR if you prefer.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/serve/grpc/v1alpha/query.rs (1)

873-883: ⚡ Quick win

Add regression tests for block_ref population in UTxO responses.

Line 873 and Line 927 introduce new behavior (prefetch + per-item mapping), but there’s no direct test here covering: populated block_ref, missing archive entry (None), and lookup failure propagation semantics.

Also applies to: 927-937

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/serve/grpc/v1alpha/query.rs` around lines 873 - 883, Add regression tests
covering the new block_ref behavior introduced by fetch_block_refs and per-item
mapping into_u5c_utxo: write tests that (1) verify a populated block_ref is
returned when fetch_block_refs yields an entry (use to_chain_point expectation),
(2) verify block_ref is None when the archive lookup returns no entry, and (3)
verify lookup failures propagate as a gRPC/internal error (map_err path). Target
the code paths invoking crate::serve::grpc::block_refs::fetch_block_refs, the
loop that calls into_u5c_utxo, and the to_chain_point conversion so the tests
exercise both the prefetch map usage and the per-item mapping/error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/serve/grpc/v1alpha/query.rs`:
- Around line 873-883: Add regression tests covering the new block_ref behavior
introduced by fetch_block_refs and per-item mapping into_u5c_utxo: write tests
that (1) verify a populated block_ref is returned when fetch_block_refs yields
an entry (use to_chain_point expectation), (2) verify block_ref is None when the
archive lookup returns no entry, and (3) verify lookup failures propagate as a
gRPC/internal error (map_err path). Target the code paths invoking
crate::serve::grpc::block_refs::fetch_block_refs, the loop that calls
into_u5c_utxo, and the to_chain_point conversion so the tests exercise both the
prefetch map usage and the per-item mapping/error handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83ccb622-d1a9-4b48-bfc4-eff4e21d129e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f438fb and b3b1495.

📒 Files selected for processing (5)
  • crates/core/src/async_query.rs
  • src/serve/grpc/block_refs.rs
  • src/serve/grpc/mod.rs
  • src/serve/grpc/v1alpha/query.rs
  • src/serve/grpc/v1beta/query.rs

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.

1 participant