Skip to content

feat(utxorpc): add block to WatchTx#756

Open
nelsonksh wants to merge 7 commits intotxpipe:mainfrom
nelsonksh:feat/add-block-to-watch-tx
Open

feat(utxorpc): add block to WatchTx#756
nelsonksh wants to merge 7 commits intotxpipe:mainfrom
nelsonksh:feat/add-block-to-watch-tx

Conversation

@nelsonksh
Copy link

@nelsonksh nelsonksh commented Oct 12, 2025

Summary by CodeRabbit

  • New Features

    • Transaction watch stream can now optionally include the full originating block when requested, providing richer context for consumers.
    • Cardano block data and native serialized bytes are embedded alongside transactions for improved traceability and analysis.
  • Chore

    • Backward-compatible: existing clients remain unaffected unless they request the extra block payload.

@nelsonksh nelsonksh requested a review from scarmuega as a code owner October 12, 2025 16:01
@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change updates block_to_txs in src/serve/grpc/watch.rs to conditionally populate AnyChainTx.block with an AnyChainBlock (setting native_bytes from the block body and chain = Cardano(mapper.map_block_cbor(body))) when the request field mask requests it. The private function parameter name changed from block to raw_block.

Changes

Cohort / File(s) Summary
gRPC watch: conditional block embedding
src/serve/grpc/watch.rs
block_to_txs(raw_block: &RawBlock, ...) now computes include_block from the request field mask and, if true, sets AnyChainTx.block = Some(AnyChainBlock { native_bytes: body.to_vec().into(), chain: Some(Cardano(mapper.map_block_cbor(body))) }); otherwise block = None. Parameter renamed from block to raw_block.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I munched on CBOR by moonlit nook,
stitched bytes in bundles—what a look!
A field mask whispered, "share a bite,"
so I tucked the block in each small byte.
— a rabbit, hopping through the code tonight 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(utxorpc): add block to WatchTx' accurately and concisely describes the main change: adding block data to WatchTx responses. It directly corresponds to the primary objective of the 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
  • Post copyable unit tests in a comment

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

@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.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee4e732 and af80f39.

📒 Files selected for processing (1)
  • src/serve/grpc/watch.rs (1 hunks)

@scarmuega scarmuega force-pushed the main branch 2 times, most recently from 295b558 to 09d0b81 Compare January 28, 2026 07:14
@scarmuega
Copy link
Member

@nelsonksh this is very reasonable, but I'm afraid that without support for field masks, the performance penalty of those who don't need the block can be prohibitive.

Solving field mask should be a cross-cut concern, since it's needed in many endpoint, but it's particularly important in this one. Maybe we could implement in ad-hoc here while we wait for the long-term solution. Just a quick check: only if the field masks include the "block" field, we inject the data.

Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/serve/grpc/watch.rs (1)

154-173: ⚠️ Potential issue | 🟠 Major

Gate AnyChainTx.block by field mask; current code always injects it.

Line 168 unconditionally sets block: Some(...), which conflicts with the stated requirement to include block data only when the request field mask contains "block". Also, Line 171 remaps the same block CBOR per transaction.

Proposed refactor
 fn block_to_txs<C: LedgerContext>(
     block: &RawBlock,
     mapper: &interop::Mapper<C>,
     request: &u5c::watch::WatchTxRequest,
 ) -> Vec<u5c::watch::AnyChainTx> {
-    let body: &BlockBody = &block;
-    let block = MultiEraBlock::decode(block).unwrap();
-    let txs = block.txs();
+    let raw_block: &BlockBody = block;
+    let decoded_block = MultiEraBlock::decode(raw_block).unwrap();
+    let txs = decoded_block.txs();
+
+    let include_block = request
+        .field_mask
+        .as_ref()
+        .is_some_and(|m| m.paths.iter().any(|p| p == "block"));
+
+    let mapped_block = include_block.then(|| u5c::watch::AnyChainBlock {
+        native_bytes: raw_block.to_vec().into(),
+        chain: Some(u5c::watch::any_chain_block::Chain::Cardano(
+            mapper.map_block_cbor(raw_block),
+        )),
+    });

     txs.iter()
         .map(|x: &pallas::ledger::traverse::MultiEraTx<'_>| mapper.map_tx(x))
         .filter(|tx| {
             request
                 .predicate
                 .as_ref()
                 .is_none_or(|predicate| apply_predicate(predicate, tx))
         })
         .map(|x| u5c::watch::AnyChainTx {
             chain: Some(u5c::watch::any_chain_tx::Chain::Cardano(x)),
-            block: Some(u5c::watch::AnyChainBlock {
-                native_bytes: body.to_vec().into(),
-                chain: Some(u5c::watch::any_chain_block::Chain::Cardano(
-                    mapper.map_block_cbor(body),
-                )),
-            }),
+            block: mapped_block.clone(),
         })
         .collect()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/serve/grpc/watch.rs` around lines 154 - 173, The code currently always
sets u5c::watch::AnyChainTx.block per transaction; change this so you first
detect whether the request's field mask includes "block" (e.g. inspect
request.field_mask or helper that checks field names), compute the block CBOR
and native_bytes once (call mapper.map_block_cbor(body) and body.to_vec())
before iterating txs, and then when building each AnyChainTx set block:
Some(...) only if the mask indicates "block" is requested, otherwise set block:
None; update the mapping expression that builds AnyChainTx.block to use the
precomputed values instead of remapping per transaction (refer to
AnyChainTx.block and mapper.map_block_cbor).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/serve/grpc/watch.rs`:
- Around line 154-173: The code currently always sets
u5c::watch::AnyChainTx.block per transaction; change this so you first detect
whether the request's field mask includes "block" (e.g. inspect
request.field_mask or helper that checks field names), compute the block CBOR
and native_bytes once (call mapper.map_block_cbor(body) and body.to_vec())
before iterating txs, and then when building each AnyChainTx set block:
Some(...) only if the mask indicates "block" is requested, otherwise set block:
None; update the mapping expression that builds AnyChainTx.block to use the
precomputed values instead of remapping per transaction (refer to
AnyChainTx.block and mapper.map_block_cbor).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2469b06 and ad689eb.

📒 Files selected for processing (1)
  • src/serve/grpc/watch.rs

Copy link

@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.

Actionable comments posted: 1

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

172-177: Precompute AnyChainBlock once per block, not once per tx.

Line 176 remaps identical block CBOR for every transaction. Hoisting this out of the tx loop reduces repeated CPU/allocation work when include_block is true.

♻️ Proposed refactor
     let body: &BlockBody = &raw_block;
     let block = MultiEraBlock::decode(raw_block).unwrap();
+    let mapped_block = include_block.then(|| u5c::watch::AnyChainBlock {
+        native_bytes: body.to_vec().into(),
+        chain: Some(u5c::watch::any_chain_block::Chain::Cardano(
+            mapper.map_block_cbor(body),
+        )),
+    });
     let txs = block.txs();
@@
         .map(|x| u5c::watch::AnyChainTx {
             chain: Some(u5c::watch::any_chain_tx::Chain::Cardano(x)),
-            block: if include_block {
-                Some(u5c::watch::AnyChainBlock {
-                    native_bytes: body.to_vec().into(),
-                    chain: Some(u5c::watch::any_chain_block::Chain::Cardano(
-                        mapper.map_block_cbor(body),
-                    )),
-                })
-            } else {
-                None
-            },
+            block: mapped_block.clone(),
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/serve/grpc/watch.rs` around lines 172 - 177, The code remaps the same
block CBOR for every transaction by constructing u5c::watch::AnyChainBlock
inside the tx loop; instead, precompute the AnyChainBlock once when
include_block is true (call mapper.map_block_cbor(body) and build the
u5c::watch::AnyChainBlock value once) and reuse that value for each
transaction’s block field (clone or reference as appropriate) so you avoid
repeating the CPU/alloc work in the tx loop; update the logic that currently
builds AnyChainBlock inline to use the precomputed variable (referencing
AnyChainBlock, mapper.map_block_cbor, and include_block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/serve/grpc/watch.rs`:
- Around line 154-156: The current include_block calculation uses substring
matching (mask.paths.iter().any(|p| p.contains("block"))) which can incorrectly
match names like "blockchain"; update the check to follow FieldMask semantics by
matching full path "block" and also allowing nested fields via prefix "block." —
i.e., in the closure over mask.paths replace the contains("block") test with a
combined exact or prefix check (p == "block" || p.starts_with("block.")) so
include_block only becomes true for the actual "block" field or its subfields;
keep the surrounding logic using request.field_mask and mask.paths unchanged.

---

Nitpick comments:
In `@src/serve/grpc/watch.rs`:
- Around line 172-177: The code remaps the same block CBOR for every transaction
by constructing u5c::watch::AnyChainBlock inside the tx loop; instead,
precompute the AnyChainBlock once when include_block is true (call
mapper.map_block_cbor(body) and build the u5c::watch::AnyChainBlock value once)
and reuse that value for each transaction’s block field (clone or reference as
appropriate) so you avoid repeating the CPU/alloc work in the tx loop; update
the logic that currently builds AnyChainBlock inline to use the precomputed
variable (referencing AnyChainBlock, mapper.map_block_cbor, and include_block).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad689eb and 93b333f.

📒 Files selected for processing (1)
  • src/serve/grpc/watch.rs

Copy link

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/serve/grpc/watch.rs`:
- Line 159: Replace the risky unwrap on MultiEraBlock::decode(raw_block) inside
the watch request handler: instead of let block =
MultiEraBlock::decode(raw_block).unwrap(); match or propagate the Result from
MultiEraBlock::decode(raw_block) (e.g., using match or .map_err()) and handle
the Err case by logging the decode error and returning a safe fallback response
(or an appropriate gRPC Status error) rather than panicking; update references
to the block variable accordingly so downstream code only runs when decode
succeeded and ensure the watch stream continues/returns a controlled error on
decode failures.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93b333f and e031952.

📒 Files selected for processing (1)
  • src/serve/grpc/watch.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.

3 participants