Skip to content

Conversation

@hrmtm23
Copy link

@hrmtm23 hrmtm23 commented Aug 15, 2025

  • Implement Read Genesis and Read Era Summary
  • Modified block structure: added timestamp, and modified name index to slot

Summary by CodeRabbit

  • New Features

    • CAIP-2 network identifiers added to API responses.
    • Ledger tip and block references now include computed timestamps.
  • Bug Fixes

    • More accurate block heights and timestamps across tip info, genesis/era summaries, and watch/transaction streams using era-aware protocol parameter calculations.
    • Added robust fallback handling for missing or undecodable block data to improve reliability.

@hrmtm23 hrmtm23 requested a review from scarmuega as a code owner August 15, 2025 16:01
@hrmtm23 hrmtm23 changed the title feat(grpc): implement utxorpc spec v0.17.0 feat(grpc): implement Read Era Summary and Read Genesis Oct 13, 2025
@hrmtm23 hrmtm23 force-pushed the feat/implement-utxorpc-spec-v17 branch from e1a0702 to 0ab83df Compare October 24, 2025 16:24
@coderabbitai
Copy link

coderabbitai bot commented Oct 24, 2025

Walkthrough

Refactors gRPC services to use domain-aware methods for chain-point/block-reference conversion and era-aware timestamp computation. point_to_u5c becomes a private method on QueryServiceImpl, get_caip2_identifier is added, point_to_blockref is added to SyncServiceImpl, and watch handlers now accept a domain parameter for timestamp and block mapping.

Changes

Cohort / File(s) Summary
Query service changes
src/serve/grpc/query.rs
Moved point_to_u5c from a public free function to a private method point_to_u5c(&self, point: &ChainPoint) -> Result<u5c::query::ChainPoint, Status> on QueryServiceImpl. Added private free fn get_caip2_identifier(network_magic: u32) -> String. Updated read handlers (read_genesis, read_era_summary, read_tx, read_utxos, read_params, search_utxos, etc.) to use the new method, compute current protocol params up to tip, derive era params, populate ledger_tip, and map CAIP-2 identifiers. Added tracing/info logging and updated error propagation paths.
Sync service changes
src/serve/grpc/sync.rs
Added instance method point_to_blockref(&self, point: &ChainPoint) -> u5c::sync::BlockRef that derives block refs including height by decoding CBOR MultiEraBlock when available (fallbacks applied). raw_to_blockref now decodes height from body with a fallback. read_tip now constructs a dynamic tip via self.point_to_blockref(&point) and computes era-aware timestamps (Origin=0; Slot/Specific derive pparams, fold into era summary, compute canonical slot_time).
Watch service changes
src/serve/grpc/watch.rs
Added domain-aware context: changed signatures to accept domain: &D (generic D: Domain, D::State: LedgerContext) for block_to_txs and roll_to_watch_response. block_to_txs now uses domain to obtain protocol params and era summary to compute block timestamps and builds AnyChainBlock/AnyChainTx with header+timestamp. Propagated domain through watch stream construction and call sites.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SyncService
    participant Domain
    participant Era
    Note over SyncService: read_tip flow (new)
    Client->>SyncService: read_tip()
    SyncService->>SyncService: point_to_blockref(chain_point)
    alt Origin
        SyncService->>SyncService: timestamp = 0
    else Slot/Specific
        SyncService->>Domain: get_pparams(up to slot)
        Domain-->>SyncService: protocol params
        SyncService->>Era: fold pparams -> chain summary
        Era-->>SyncService: canonical slot_time
        SyncService->>SyncService: timestamp = slot_time as u64
    end
    SyncService->>Client: ReadTipResponse(tip, timestamp)
Loading
sequenceDiagram
    participant Client
    participant WatchService
    participant Domain
    participant BlockProcessor
    Note over WatchService: watch_tx stream (new)
    Client->>WatchService: watch_tx(request)
    loop for each TipEvent
        WatchService->>WatchService: roll_to_watch_response(domain, mapper, log, request)
        WatchService->>BlockProcessor: block_to_txs(block, domain, mapper, request)
        BlockProcessor->>Domain: get protocol params & era summary
        Domain-->>BlockProcessor: params + era context
        BlockProcessor->>BlockProcessor: compute block timestamp (era-aware)
        BlockProcessor-->>WatchService: AnyChainTx[] (header + timestamp)
        WatchService-->>Client: WatchTxResponse
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Reason: Non-trivial refactors across three service layers with new era-aware parameter folding, block decoding logic, and API-surface changes requiring cross-file consistency checks.
  • Pay extra attention to:
    • Error path conversions to Status (into_status) in point_to_u5c and block decoding.
    • Correctness of era-aware pparams folding and timestamp computation.
    • All updated call sites for the new method signatures (watch/service roll and block mapping).
    • Fallback behaviors when block decoding or pparams lookup fails.

Suggested reviewers

  • scarmuega

Poem

🐰
I hopped through code with careful paws,
mapped slots to times and fixed the laws,
CAIP-2 named the network bright,
blocks now carry timestamps right.
A tiny hop — the services hum with applause.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat(grpc): implement Read Era Summary and Read Genesis" directly aligns with the primary objectives stated in the PR description, which explicitly mention implementing Read Genesis and Read Era Summary. The raw summary confirms that the main changes in src/serve/grpc/query.rs involve expanding the read_genesis and read_era_summary workflows to compute current protocol parameters and map genesis/era summaries. While the PR also includes supporting changes to sync.rs and watch.rs, the title appropriately focuses on the primary feature being implemented rather than attempting to enumerate all modifications. The title is concise, clear, and uses specific terminology without vague or generic phrasing.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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: 4

🧹 Nitpick comments (5)
src/serve/grpc/sync.rs (3)

43-57: Minor: double-decode to get height; consider single source of truth.

You decode MultiEraBlock solely to read number() after already mapping the block header. If the mapped header exposes the height, use it; otherwise, consider decoding only when header is present to avoid redundant work. Not a blocker.


112-143: Two BlockRef helpers diverge; unify to avoid inconsistent height.

You now have:

  • free fn point_to_blockref (Line 60) → height defaulted via Default::default()
  • method self.point_to_blockref(...) (Lines 112-143) → best-effort real height

Tip reset events still call the free fn, yielding height=0 while other paths provide real height. Unify by removing the free fn and routing all call sites through the method (e.g., make tip_event_to_response a method or pass a closure).


261-285: Timestamp logic OK; extract shared helper to de-duplicate.

Same era-aware flow appears in query.rs and watch.rs. Extract a small helper, e.g., fn slot_timestamp<D: Domain + LedgerContext>(domain: &D, slot: u64) -> u64, and reuse.

src/serve/grpc/query.rs (1)

480-507: De-duplicate era-aware pparams folding and timestamp/era derivation.

The update→fold_with_hacks→era_for_slot flow is duplicated here and elsewhere. Extract a shared utility and reuse to reduce complexity and drift.

Also applies to: 546-566

src/serve/grpc/watch.rs (1)

164-186: LGTM: era‑aware timestamp derivation. Consider centralizing helper.

Same logic appears in sync/query; extract a shared slot_timestamp helper to keep behavior consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f98781f and 0ab83df.

📒 Files selected for processing (3)
  • src/serve/grpc/query.rs (7 hunks)
  • src/serve/grpc/sync.rs (3 hunks)
  • src/serve/grpc/watch.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/serve/grpc/sync.rs (2)
crates/core/src/point.rs (1)
  • slot (16-22)
crates/cardano/src/eras.rs (2)
  • slot_time (19-23)
  • slot_time (68-71)
src/serve/grpc/watch.rs (3)
src/serve/grpc/query.rs (4)
  • dolos_cardano (302-302)
  • updates (75-77)
  • updates (496-499)
  • updates (554-557)
src/serve/grpc/sync.rs (2)
  • updates (272-274)
  • stream (237-237)
crates/cardano/src/eras.rs (2)
  • slot_time (19-23)
  • slot_time (68-71)
src/serve/grpc/query.rs (4)
crates/core/src/lib.rs (5)
  • network_magic (256-261)
  • point (501-505)
  • slot (497-497)
  • genesis (710-710)
  • hash (498-498)
crates/core/src/point.rs (2)
  • slot (16-22)
  • hash (24-29)
src/serve/grpc/sync.rs (1)
  • updates (272-274)
crates/cardano/src/eras.rs (2)
  • slot_time (19-23)
  • slot_time (68-71)
🔇 Additional comments (7)
src/serve/grpc/sync.rs (1)

287-290: LGTM: tip now includes domain-aware height via self.point_to_blockref.

src/serve/grpc/query.rs (4)

315-315: LGTM: ledger_tip via self.point_to_u5c for read_params.


365-366: LGTM: ledger_tip via self.point_to_u5c for read_utxos.


453-454: LGTM: ledger_tip via self.point_to_u5c for read_tx.


570-575: LGTM: era summaries mapped through mapper with current params.

src/serve/grpc/watch.rs (2)

212-220: LGTM: domain-aware plumbing for roll_to_watch_response.


294-295: LGTM: stream composition with flat_map and cloned domain.

Comment on lines +13 to 22
/// Get the CAIP-2 blockchain identifier for the given network magic
fn get_caip2_identifier(network_magic: u32) -> String {
let network_magic = network_magic as u64;
match network_magic {
MAINNET_MAGIC => format!("cardano-mainnet:{}", network_magic),
TESTNET_MAGIC => format!("cardano-testnet:{}", network_magic),
PREVIEW_MAGIC => format!("cardano-preview:{}", network_magic),
PRE_PRODUCTION_MAGIC => format!("cardano-preprod:{}", network_magic),
_ => format!("cardano:{}", network_magic), // fallback for unknown networks
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm CAIP‑2 identifier format.

Mapping to strings like "cardano-mainnet:{magic}" may not match CAIP‑2 conventions or UTxO‑RPC v1.7 expectations. Please verify the exact namespace/reference format required by the spec and adjust accordingly.


🌐 Web query:

What CAIP-2 chain ID format should Cardano networks (mainnet, preprod, preview, testnet) use in UTxO-RPC v1.7’s ReadGenesisResponse.caip2 field? Provide examples and authoritative source.

💡 Result:

Use a CAIP‑2 chain id of the form : (CAIP‑2). For Cardano networks the canonical namespace is "cardano" and the human-readable references are the network names. Examples for ReadGenesisResponse.caip2 (UTxO‑RPC v1.7) are:

  • cardano:mainnet
  • cardano:preprod
  • cardano:preview
  • cardano:testnet

Authoritative reference: the CAIP‑2 specification (chain id syntax: namespace:reference). [1]

Sources

  1. CAIP‑2: Blockchain ID Specification (ChainAgnostic). [1]

Fix CAIP-2 identifier format to match UTxO-RPC v1.7 spec.

The current format includes the network magic number and incorrect namespace structure. CAIP-2 specifies the format as <namespace>:<reference>, with Cardano using namespace "cardano" and references as network names. Update the format strings to:

MAINNET_MAGIC => "cardano:mainnet",
TESTNET_MAGIC => "cardano:testnet",
PREVIEW_MAGIC => "cardano:preview",
PRE_PRODUCTION_MAGIC => "cardano:preprod",
_ => format!("cardano:{}", network_magic), // fallback using magic as reference

Remove the network magic from mainnet/testnet/preview/preprod identifiers; only include it in the fallback for unknown networks.

🤖 Prompt for AI Agents
In src/serve/grpc/query.rs around lines 13 to 22, the CAIP-2 identifiers are
currently formatted with the network magic number and incorrect namespace
structure; update the match arms so known networks return the canonical CAIP-2
references (use "cardano:mainnet", "cardano:testnet", "cardano:preview",
"cardano:preprod") and only use the network_magic value in the fallback (e.g.,
format!("cardano:{}", network_magic)) so the mainnet/testnet/preview/preprod
cases do not include the magic number.

Comment on lines 43 to 96
fn point_to_u5c(&self, point: &ChainPoint) -> u5c::query::ChainPoint {
match point {
ChainPoint::Origin => u5c::query::ChainPoint {
slot: 0,
hash: vec![].into(),
height: 0,
timestamp: 0,
},
ChainPoint::Slot(slot) | ChainPoint::Specific(slot, _) => {
// Calculate height by looking up block from slot
let height = self.domain
.archive()
.get_block_by_slot(slot)
.map(|block| {
block.map(|body| {
// Parse the block to get the height
use pallas::ledger::traverse::MultiEraBlock;
if let Ok(parsed_block) = MultiEraBlock::decode(&body) {
parsed_block.number()
} else {
*slot // Fallback to slot
}
}).unwrap_or(*slot)
})
.unwrap_or(*slot);

// Calculate timestamp from slot using proper era handling
// Get protocol parameter updates up to this slot
let updates = self.domain.state()
.get_pparams(*slot)
.ok()
.and_then(|updates| {
updates.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()
.ok()
})
.unwrap_or_default();

// Get chain summary with proper era handling
let summary = pparams::fold_with_hacks(self.domain.genesis(), &updates, *slot);

// Calculate timestamp using the canonical function
let timestamp = dolos_cardano::slot_time(*slot, &summary) as u64;

u5c::query::ChainPoint {
slot: *slot,
hash: point.hash().map(|h| h.to_vec()).unwrap_or_default().into(),
height,
timestamp,
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Build break: pparams used without import.

pparams::fold_with_hacks is referenced but dolos_cardano::pparams isn’t imported in this file, causing a compile error. Add the import once near the top.

Apply this diff:

@@
 use super::masking::apply_mask;
 use crate::prelude::*;
+use dolos_cardano::pparams;
 use pallas::ledger::traverse::wellknown::{MAINNET_MAGIC, TESTNET_MAGIC, PREVIEW_MAGIC, PRE_PRODUCTION_MAGIC};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/serve/grpc/query.rs around lines 43 to 96, the code references
pparams::fold_with_hacks but never imports dolos_cardano::pparams, causing a
compile error; add a single use import near the top of the file (e.g., use
dolos_cardano::pparams;) so pparams::fold_with_hacks resolves, then run cargo
build to verify the file compiles.

Comment on lines +158 to +160
let parsed_block = MultiEraBlock::decode(block).unwrap();
let txs = parsed_block.txs();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Panic risk: unwrap on block decode in hot path.

MultiEraBlock::decode(block).unwrap() can panic on malformed data and take down the stream/server. Handle the error and skip/propagate safely.

Apply this diff:

-    let parsed_block = MultiEraBlock::decode(block).unwrap();
+    let parsed_block = match MultiEraBlock::decode(block) {
+        Ok(b) => b,
+        Err(e) => {
+            tracing::warn!(error = %e, "failed to decode block in watch; skipping txs");
+            return Vec::new();
+        }
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let parsed_block = MultiEraBlock::decode(block).unwrap();
let txs = parsed_block.txs();
let parsed_block = match MultiEraBlock::decode(block) {
Ok(b) => b,
Err(e) => {
tracing::warn!(error = %e, "failed to decode block in watch; skipping txs");
return Vec::new();
}
};
let txs = parsed_block.txs();

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

♻️ Duplicate comments (2)
src/serve/grpc/query.rs (2)

13-22: CAIP-2 format remains incorrect—remove magic from known networks.

This critical issue was flagged in a previous review but not yet addressed. The format should be cardano:mainnet, cardano:testnet, etc., without the magic number suffix for known networks. Only the fallback for unknown networks should include the magic.

Apply this diff:

 fn get_caip2_identifier(network_magic: u32) -> String {
     let network_magic = network_magic as u64;
     match network_magic {
-        MAINNET_MAGIC => format!("cardano-mainnet:{}", network_magic),
-        TESTNET_MAGIC => format!("cardano-testnet:{}", network_magic),
-        PREVIEW_MAGIC => format!("cardano-preview:{}", network_magic),
-        PRE_PRODUCTION_MAGIC => format!("cardano-preprod:{}", network_magic),
+        MAINNET_MAGIC => "cardano:mainnet".to_string(),
+        TESTNET_MAGIC => "cardano:testnet".to_string(),
+        PREVIEW_MAGIC => "cardano:preview".to_string(),
+        PRE_PRODUCTION_MAGIC => "cardano:preprod".to_string(),
         _ => format!("cardano:{}", network_magic), // fallback for unknown networks
     }
 }

43-94: Build break: pparams module not imported.

This critical issue was flagged in a previous review but remains unaddressed. Line 81 references pparams::fold_with_hacks but dolos_cardano::pparams is not imported, causing a compilation error.

Add the import near the top of the file:

 use super::masking::apply_mask;
 use crate::prelude::*;
+use dolos_cardano::pparams;
 use pallas::ledger::traverse::wellknown::{MAINNET_MAGIC, TESTNET_MAGIC, PREVIEW_MAGIC, PRE_PRODUCTION_MAGIC};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab83df and 961cd8a.

📒 Files selected for processing (1)
  • src/serve/grpc/query.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/serve/grpc/query.rs (5)
crates/core/src/lib.rs (4)
  • network_magic (256-261)
  • point (501-505)
  • slot (497-497)
  • genesis (710-710)
crates/core/src/point.rs (1)
  • slot (16-22)
src/serve/grpc/sync.rs (1)
  • updates (272-274)
src/adapters.rs (1)
  • genesis (61-63)
crates/cardano/src/eras.rs (2)
  • slot_time (19-23)
  • slot_time (68-71)
🔇 Additional comments (3)
src/serve/grpc/query.rs (3)

312-312: LGTM—consistent use of the new method.

The migration from the old standalone function to self.point_to_u5c is correctly implemented across all query handlers, with proper error propagation.

Also applies to: 363-364, 411-412, 453-454


519-519: Genesis hash correctly sourced.

Line 519 now properly uses genesis.shelley_hash.to_vec() instead of deriving from the cursor, matching the canonical genesis identifier for Cardano.


490-508: Consider failing when tip is unavailable.

When no tip exists, current_params silently falls back to None, potentially masking initialization issues. Per a previous review comment, consider returning Status::internal or Status::unavailable instead of proceeding with incomplete data, especially since genesis configuration should be static and always available.

Run the following to check if a missing tip represents a valid state:

Comment on lines +540 to +558
let current_params = if let Some(tip_point) = tip.as_ref() {
let updates = self
.domain
.state()
.get_pparams(tip_point.slot())
.map_err(into_status)?;

let updates: Vec<_> = updates
.into_iter()
.map(TryInto::try_into)
.try_collect::<_, _, pallas::codec::minicbor::decode::Error>()
.map_err(|e| Status::internal(e.to_string()))?;

let summary = pparams::fold_with_hacks(genesis, &updates, tip_point.slot());
let era = summary.era_for_slot(tip_point.slot());
Some(era.pparams.clone())
} else {
None
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider failing when tip is unavailable.

Similar to read_genesis, this handler silently sets current_params to None when the tip is unavailable. Era summaries are structural chain metadata that should always be available. Consider returning an error status instead of proceeding with incomplete parameters.

🤖 Prompt for AI Agents
In src/serve/grpc/query.rs around lines 540 to 558, the code currently sets
current_params to None when tip is unavailable; change this to return an
Err(Status::internal(...)) (or Status::not_found(...) per project's convention)
so the RPC fails instead of proceeding with incomplete era parameters.
Concretely, check if tip.is_none() and return an appropriate Status error with a
clear message (e.g., "tip/era summary unavailable") before attempting to derive
pparams; keep the rest of the existing pparams extraction logic unchanged.

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.

2 participants