Skip to content

fix: Improve conformance tests coverage - #853

Merged
scarmuega merged 12 commits into
mainfrom
fix/improve-conformance-tests-coverage
Feb 3, 2026
Merged

fix: Improve conformance tests coverage#853
scarmuega merged 12 commits into
mainfrom
fix/improve-conformance-tests-coverage

Conversation

@gonzalezzfelipe

@gonzalezzfelipe gonzalezzfelipe commented Jan 30, 2026

Copy link
Copy Markdown
Contributor
  • add metadata to pool on /txs/hash/pool_updates
  • Only show lovelace on utxos used as collateral

Summary by CodeRabbit

  • New Features

    • Fetch and display pool off-chain metadata (name, ticker, description, homepage)
    • DRep voting support for Conway-era transactions
    • Genesis-aware handling for preview/preprod networks in block endpoints
    • Historical UTXO lookup exposed on domain adapters
  • Bug Fixes

    • More accurate epoch calculation and slot/leader formatting
    • Safer protocol-parameter / cost-model handling and relaxed min-UTxO logic
    • Improved UTXO and delegator filtering (includes retired pools)
  • Chores

    • Updated core dependency version

@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds asynchronous per-pool off‑chain metadata fetching and propagation into transaction/pool models, introduces a hacks module for domain-aware genesis handling, expands Conway DRep voting collection, adjusts epoch/pparams and cost-model serialization, and moves historical UTXO lookup into DomainAdapter.

Changes

Cohort / File(s) Summary
Pool off‑chain metadata
crates/minibf/src/mapping.rs, crates/minibf/src/routes/pools.rs, crates/minibf/src/routes/txs.rs
Introduce PoolOffchainMetadata and pool_offchain_metadata(url); add pool_metadata: HashMap<PoolHash, PoolOffchainMetadata> to TxModelBuilder; add fetch_pool_metadata() to fetch per-pool data in parallel and wire offchain fields into PoolUpdateModelBuilder and pool certificate metadata; routes call fetch before responses.
Genesis / hacks
crates/minibf/src/hacks.rs, crates/minibf/src/routes/blocks.rs, crates/minibf/src/lib.rs, crates/cardano/src/hacks.rs
Add hacks module with domain-aware genesis constants and helpers; refactor block routes to use hacks for genesis (block 0), paging, and to set genesis previous linkage; remove prior in-file genesis-preview helper.
DRep voting & governance
crates/cardano/src/roll/dreps.rs, crates/minibf/src/routes/governance.rs
Add visit_tx to collect Conway voting activity into DRep deltas; narrow certificate DRep extraction to Conway path; change parse_drep_id return shape and make DrepModelBuilder.state optional with is_legacy flag.
Epochs & protocol params
crates/minibf/src/routes/epochs/mapping.rs, crates/minibf/src/lib.rs, crates/cardano/src/forks.rs
Derive epoch via chain.slot_epoch(...).0; change cost-model mapping functions to return optional shapes; add decentralisation_param; relax min_utxo fallback; adjust fork cost-model selection and AdaPerUtxoByte handling.
Historical UTXO access
src/adapters/mod.rs, crates/testing/src/toy_domain.rs
Move get_historical_utxos implementation into DomainAdapter::get_historical_utxos (scans WAL backwards); remove stub from ToyDomain LedgerContext impl.
Misc & infra
Cargo.toml, tests/external/smoke.rs, tests/external/scenarios/mainnet-20-blocks/dolos.toml, tests/cardano/harness/compare.rs
Bump pallas git revision; add dbg! in smoke test; increase sync MaxBlocks (25→50); minor formatting change in compare.rs; add small preview proposal→Ratified mapping in cardano hacks.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Routes as Routes/Txs
    participant Builder as TxModelBuilder
    participant API as Offchain API
    Client->>Routes: Request tx by hash
    Routes->>Builder: create builder (with_chain/with_network)
    Builder->>Builder: init pool_metadata HashMap
    Routes->>Builder: await Builder.fetch_pool_metadata()
    Builder->>API: parallel fetch per-pool URLs (join_all)
    API-->>Builder: return PoolOffchainMetadata results
    Builder->>Builder: populate pool_metadata map
    Builder->>Builder: wire offchain into PoolUpdateModelBuilder
    Builder-->>Routes: enriched tx model
    Routes-->>Client: response with pool metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • scarmuega

Poem

🐰 Hopping through hashes with a whiskered tap,

I fetch offchain petals and tuck them in a map,
Conway votes I count, genesis wakes from sleep,
previous links stitched softly, seeds of data keep,
A rabbit in the diff — joyful, nimble, and apt.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.68% 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 'fix: Improve conformance tests coverage' directly relates to the main changes which focus on improving pool metadata handling, deposit calculations, and collateral filtering in pool updates and transaction responses.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/improve-conformance-tests-coverage

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@crates/minibf/src/mapping.rs`:
- Around line 1652-1659: Remove the debug macro call dbg!(&meta) in the closure
that builds pool updates: inside the builder.map closure created from
PoolUpdateModelBuilder::new(...) (the block that fetches meta from
self.pool_metadata and calls x.with_offchain(meta)), delete the dbg!(&meta);
line so the code no longer prints to stderr during pool registration processing;
if you need non-debug logging, replace it with a proper logger call instead of
dbg!.
- Around line 136-150: The pool_offchain_metadata function currently fetches
user-controlled URLs directly, risking SSRF; before calling
client.get(url).send() in pool_offchain_metadata, parse and validate the URL
(use url::Url) to allow only http/https schemes, reject file:// and other
schemes, and ensure the host isn't a loopback, link-local, or private/reserved
IP or resolves to such IPs (perform DNS resolution and check IP ranges, and
reject raw IP literals in private ranges). If validation fails, return None;
otherwise proceed to build the reqwest client and send the request. Ensure
validation logic is applied inside the pool_offchain_metadata function prior to
making the outbound request.

Comment on lines +136 to +150
pub async fn pool_offchain_metadata(url: &str) -> Option<PoolOffchainMetadata> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.user_agent("Dolos MiniBF")
.build()
.ok()?;

let res = client.get(url).send().await.ok()?;

if res.status() != StatusCode::OK {
return None;
}

res.json().await.ok()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider URL validation to mitigate SSRF risk.

The pool_offchain_metadata function directly fetches from user-controlled URLs stored in pool registration certificates. While pool metadata URLs are part of on-chain data, consider adding URL validation (e.g., disallowing private IP ranges, localhost, file:// schemes) to reduce server-side request forgery (SSRF) exposure.

🛡️ Suggested URL validation approach
 pub async fn pool_offchain_metadata(url: &str) -> Option<PoolOffchainMetadata> {
+    // Basic URL validation to prevent SSRF
+    let parsed = url::Url::parse(url).ok()?;
+    if !matches!(parsed.scheme(), "http" | "https") {
+        return None;
+    }
+
     let client = reqwest::Client::builder()
         .timeout(Duration::from_secs(5))
         .user_agent("Dolos MiniBF")
         .build()
         .ok()?;
🤖 Prompt for AI Agents
In `@crates/minibf/src/mapping.rs` around lines 136 - 150, The
pool_offchain_metadata function currently fetches user-controlled URLs directly,
risking SSRF; before calling client.get(url).send() in pool_offchain_metadata,
parse and validate the URL (use url::Url) to allow only http/https schemes,
reject file:// and other schemes, and ensure the host isn't a loopback,
link-local, or private/reserved IP or resolves to such IPs (perform DNS
resolution and check IP ranges, and reject raw IP literals in private ranges).
If validation fails, return None; otherwise proceed to build the reqwest client
and send the request. Ensure validation logic is applied inside the
pool_offchain_metadata function prior to making the outbound request.

Comment thread crates/minibf/src/mapping.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@crates/minibf/src/routes/blocks.rs`:
- Around line 286-290: The match on domain.genesis().shelley.network_magic uses
todo!() which will panic for None (mainnet) or unexpected values; change this to
return a proper error instead of panicking. Replace the _ => todo!() arm in the
block_0 initialization with an Err variant (or create a new Error enum variant)
that wraps a descriptive message about unsupported/unknown network_magic,
ensuring it composes with the existing .map_err(Error::Code)? pattern used for
block_0_preprod and block_0_preview; keep references to block_0,
domain.genesis().shelley.network_magic, block_0_preprod, block_0_preview, and
Error::Code so callers can handle the failure rather than crashing.
- Around line 217-220: The handler currently returns the synthetic block 0 only
for preprod (network_magic == Some(1)) but not for preview (Some(2)), causing
load_block_by_hash_or_number to return NOT_FOUND; add an equivalent early return
when Either::Right(0) == hash_or_number &&
domain.genesis().shelley.network_magic == Some(2) that returns
Ok(Json(block_0_preview(&domain)?)) (mirroring the block_0_preprod branch), and
ensure the surrounding if/return block uses the same brace placement and
indentation style as the existing preprod branch and the pagination code to keep
formatting consistent.
🧹 Nitpick comments (5)
crates/minibf/src/mapping.rs (3)

128-134: Consider making fields optional to handle incomplete metadata JSON.

All fields in PoolOffchainMetadata are required String types. If a pool operator's metadata JSON is missing any of these fields (which is common), the res.json().await deserialization will fail silently (returning None). Consider using Option<String> with serde defaults to be more resilient:

♻️ Suggested change for robustness
 #[derive(Serialize, Deserialize, Clone, Debug)]
 pub struct PoolOffchainMetadata {
+    #[serde(default)]
-    pub name: String,
+    pub name: Option<String>,
+    #[serde(default)]
-    pub description: String,
+    pub description: Option<String>,
+    #[serde(default)]
-    pub ticker: String,
+    pub ticker: Option<String>,
+    #[serde(default)]
-    pub homepage: String,
+    pub homepage: Option<String>,
 }

577-628: Consider limiting concurrent metadata fetches.

The fetch_pool_metadata function uses join_all to fetch metadata for all pool registrations in parallel without any concurrency limit. While typically there are few pool registrations per transaction, a transaction with many registrations could spawn unbounded concurrent HTTP requests, potentially overwhelming the network or triggering rate limits on metadata servers.

♻️ Suggested approach using futures stream with buffer
+use futures::stream::{self, StreamExt};
+
 pub async fn fetch_pool_metadata(&mut self) -> Result<(), StatusCode> {
     // ... pool_registrations extraction ...

-    self.pool_metadata = join_all(pool_registrations.iter().map(
-        |(pool_hash, url)| async move {
-            pool_offchain_metadata(url)
-                .await
-                .map(|meta| (*pool_hash, meta))
-        },
-    ))
-    .await
-    .into_iter()
-    .flatten()
-    .collect();
+    self.pool_metadata = stream::iter(pool_registrations.iter())
+        .map(|(pool_hash, url)| async move {
+            pool_offchain_metadata(url)
+                .await
+                .map(|meta| (*pool_hash, meta))
+        })
+        .buffer_unordered(4) // Limit concurrent requests
+        .filter_map(|x| async { x })
+        .collect()
+        .await;

     Ok(())
 }

588-588: Redundant clone pattern.

The pattern *(**cow).clone() clones the entire certificate struct. Since you only need the operator and pool_metadata fields, consider extracting just those references without cloning the whole struct:

♻️ Suggested optimization
 MultiEraCert::AlonzoCompatible(cow) => {
-    if let AlonzoCert::PoolRegistration {
-        operator,
-        ref pool_metadata,
-        ..
-    } = *(**cow).clone()
+    if let AlonzoCert::PoolRegistration {
+        operator,
+        pool_metadata,
+        ..
+    } = &***cow
     {
         pool_metadata
             .as_ref()
-            .map(|meta| (operator, meta.url.clone()))
+            .map(|meta| (*operator, meta.url.clone()))
     } else {
         None
     }
 }
crates/minibf/src/routes/blocks.rs (2)

68-109: Consider extracting common logic to reduce duplication.

block_0_preprod is nearly identical to block_0_preview (lines 25-66). The only differences are the hardcoded time, hash, and next_block values. The confirmations calculation and utxo summation logic is duplicated.

Consider extracting a helper that accepts network-specific parameters:

♻️ Proposed refactor
struct GenesisBlockParams {
    time: i32,
    hash: &'static str,
    next_block: &'static str,
}

fn block_0_generic<D: Domain>(
    domain: &Facade<D>,
    params: GenesisBlockParams,
) -> Result<BlockContent, StatusCode> {
    let confirmations = MultiEraBlock::decode(
        &domain
            .archive()
            .get_tip()
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
            .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
            .1,
    )
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    .header()
    .number() as i32;

    let byron_utxos = byron::genesis_utxos(&domain.genesis().byron);
    let shelley_utxos = shelley::shelley_utxos(&domain.genesis().shelley);

    Ok(BlockContent {
        time: params.time,
        hash: params.hash.to_string(),
        next_block: Some(params.next_block.to_string()),
        // ... rest of fields
    })
}

282-284: Update stale comment.

The comment says "Insert block 0 only in preview" but the code now handles both preview (magic 2) and preprod (magic 1).

📝 Suggested comment update
-    // Insert block 0 only in preview
+    // Insert synthetic block 0 for testnets (preprod and preview)
     if output.len() < pagination.count

Comment thread crates/minibf/src/routes/blocks.rs Outdated
Comment thread crates/minibf/src/routes/blocks.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/minibf/src/routes/governance.rs`:
- Around line 43-49: The drep_script match arm currently encodes bech32 using
the original 28-byte payload but returns a 29-byte payload with
DREP_SCRIPT_PREFIX, causing a mismatch; update the bech32 call in the
("drep_script", 28) branch to use the prefixed new_payload (the same bytes you
return) instead of &payload so bech32(..., &new_payload) and the returned
[vec![DREP_SCRIPT_PREFIX], payload].concat() are consistent with the other drep
cases (refer to DREP_SCRIPT_PREFIX, new_payload, and the bech32 call in that
match arm).

Comment on lines +43 to +49
("drep_script", 28) => Ok((
bech32(bech32::Hrp::parse("drep").unwrap(), &payload)
.map_err(|_| StatusCode::BAD_REQUEST)?,
[vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat(),
true,
false,
)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent bech32 encoding for drep_script case.

The drep_script branch encodes the bech32 string with the original 28-byte payload, but returns a 29-byte payload that includes the DREP_SCRIPT_PREFIX. This is inconsistent with the drep/drep_vkh case (lines 33-41) where both the bech32 encoding and returned payload use the prefixed new_payload.

This mismatch means the drep_id string won't correspond to the drep_id_encoded bytes for script DReps, potentially causing incorrect hex representations or lookup issues downstream.

🐛 Proposed fix to use consistent payload for encoding
 ("drep_script", 28) => Ok((
-    bech32(bech32::Hrp::parse("drep").unwrap(), &payload)
-        .map_err(|_| StatusCode::BAD_REQUEST)?,
-    [vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat(),
+    {
+        let new_payload = [vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat();
+        (
+            bech32(bech32::Hrp::parse("drep").unwrap(), &new_payload)
+                .map_err(|_| StatusCode::BAD_REQUEST)?,
+            new_payload,
+        )
+    }.0,
+    {
+        let new_payload = [vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat();
+        new_payload
+    },
     true,
     false,
 )),

Or more cleanly:

-("drep_script", 28) => Ok((
-    bech32(bech32::Hrp::parse("drep").unwrap(), &payload)
-        .map_err(|_| StatusCode::BAD_REQUEST)?,
-    [vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat(),
-    true,
-    false,
-)),
+("drep_script", 28) => {
+    let new_payload = [vec![pallas_extras::DREP_SCRIPT_PREFIX], payload].concat();
+    Ok((
+        bech32(bech32::Hrp::parse("drep").unwrap(), &new_payload)
+            .map_err(|_| StatusCode::BAD_REQUEST)?,
+        new_payload,
+        true,
+        false,
+    ))
+}
🤖 Prompt for AI Agents
In `@crates/minibf/src/routes/governance.rs` around lines 43 - 49, The drep_script
match arm currently encodes bech32 using the original 28-byte payload but
returns a 29-byte payload with DREP_SCRIPT_PREFIX, causing a mismatch; update
the bech32 call in the ("drep_script", 28) branch to use the prefixed
new_payload (the same bytes you return) instead of &payload so bech32(...,
&new_payload) and the returned [vec![DREP_SCRIPT_PREFIX], payload].concat() are
consistent with the other drep cases (refer to DREP_SCRIPT_PREFIX, new_payload,
and the bech32 call in that match arm).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/external/smoke.rs`:
- Around line 234-235: Remove the leftover dbg! invocation (dbg!(&after)) from
the test; either delete the dbg! call entirely or replace it with a deliberate
diagnostic such as println! or eprintln! with a clear message referencing the
variable after if you need runtime output, then re-run cargo clippy to ensure
the dbg_macro lint is resolved.
🧹 Nitpick comments (1)
src/adapters/mod.rs (1)

61-85: Track remaining refs to avoid unnecessary WAL scans.

Line 82 breaks on result.len() == refs.len(), so duplicate refs will prevent early exit and can force a full WAL scan. Consider tracking remaining refs and breaking when the set is empty.

♻️ Suggested refactor
-        let refs_set: std::collections::HashSet<_> =
-            refs.iter().copied().map(TxoRef::from).collect();
+        let mut remaining: std::collections::HashSet<_> =
+            refs.iter().copied().map(TxoRef::from).collect();

         let iter = self.wal().iter_logs(None, None).ok()?;
         for (_, log) in iter.rev() {
             for (txo_ref, era_cbor) in &log.inputs {
-                if refs_set.contains(txo_ref) {
+                if remaining.remove(txo_ref) {
                     let era = era_cbor.0.try_into().expect("era out of range");
                     result.insert(txo_ref.clone().into(), (era, era_cbor.1.clone()));
                 }
             }

-            if result.len() == refs.len() {
+            if remaining.is_empty() {
                 break;
             }
         }

Comment thread tests/external/smoke.rs
Comment on lines +234 to +235
dbg!(&after);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove leftover dbg! statement.

The dbg! macro is a debugging artifact that should not be committed. It will be flagged by cargo clippy (via dbg_macro lint). If logging is needed for test diagnostics, consider using println! or eprintln! with a clear message, or remove it entirely.

🧹 Proposed fix
-    dbg!(&after);
-
     assert!(after.wal.tip_slot.unwrap() >= before.wal.tip_slot.unwrap_or_default() + 20);

As per coding guidelines: "Run cargo clippy --workspace --all-targets --all-features and resolve all clippy warnings before committing changes."

📝 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
dbg!(&after);
🤖 Prompt for AI Agents
In `@tests/external/smoke.rs` around lines 234 - 235, Remove the leftover dbg!
invocation (dbg!(&after)) from the test; either delete the dbg! call entirely or
replace it with a deliberate diagnostic such as println! or eprintln! with a
clear message referencing the variable after if you need runtime output, then
re-run cargo clippy to ensure the dbg_macro lint is resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/minibf/src/routes/blocks.rs (1)

179-181: ⚠️ Potential issue | 🟠 Major

Handle numeric genesis (/blocks/0) consistently in previous/next endpoints.
/blocks/0 is synthesized, but /blocks/0/previous and /blocks/0/next still go through load_block_by_hash_or_number, which can return NOT_FOUND when genesis isn’t archived. Treat Either::Right(0) as genesis: previous should return empty; next should use the genesis range path.

Suggested fix
     let hash_or_number = parse_hash_or_number(&hash_or_number)?;
+    if matches!(hash_or_number, Either::Right(0)) {
+        return Ok(Json(Vec::new()));
+    }
     let curr = load_block_by_hash_or_number(&domain, &hash_or_number).await?;
-    let is_genesis = match &hash_or_number {
-        Either::Left(hash) => {
-            hacks::is_genesis_hash_for_domain(&domain, hash).map_err(Error::Code)?
-        }
-        _ => false,
-    };
+    let is_genesis = match &hash_or_number {
+        Either::Right(0) => true,
+        Either::Left(hash) => {
+            hacks::is_genesis_hash_for_domain(&domain, hash).map_err(Error::Code)?
+        }
+        _ => false,
+    };

Also applies to: 258-263

🤖 Fix all issues with AI agents
In `@crates/minibf/src/routes/blocks.rs`:
- Around line 228-236: The code determines whether to include genesis in the
"previous" pagination using genesis_index = curr_number but that is off-by-one;
change genesis_index to curr_number.saturating_sub(1) (or otherwise subtract 1
safely) so the zero-based previous range check (from, to =
from.saturating_add(pagination.count)) correctly sets genesis_in_range and
allows the block_0.take() -> output.push(genesis) path to run for ranges that
should include genesis; update the genesis_index computation where it is
declared so genesis_in_range logic and use of block_0/output remain correct.
🧹 Nitpick comments (2)
crates/minibf/src/mapping.rs (2)

583-612: Consider avoiding unnecessary clone when pattern matching certificates.

The pattern *(**cow).clone() clones the entire certificate just to pattern match on its variant. This could be optimized by matching on references instead.

♻️ Suggested optimization
             MultiEraCert::AlonzoCompatible(cow) => {
-                if let AlonzoCert::PoolRegistration {
-                    operator,
-                    ref pool_metadata,
-                    ..
-                } = *(**cow).clone()
-                {
-                    pool_metadata
-                        .as_ref()
-                        .map(|meta| (operator, meta.url.clone()))
+                if let AlonzoCert::PoolRegistration {
+                    operator,
+                    pool_metadata,
+                    ..
+                } = &***cow
+                {
+                    pool_metadata
+                        .as_ref()
+                        .map(|meta| (*operator, meta.url.clone()))
                 } else {
                     None
                 }
             }
             MultiEraCert::Conway(cow) => {
-                if let ConwayCert::PoolRegistration {
-                    operator,
-                    ref pool_metadata,
-                    ..
-                } = *(**cow).clone()
-                {
-                    pool_metadata
-                        .as_ref()
-                        .map(|meta| (operator, meta.url.clone()))
+                if let ConwayCert::PoolRegistration {
+                    operator,
+                    pool_metadata,
+                    ..
+                } = &***cow
+                {
+                    pool_metadata
+                        .as_ref()
+                        .map(|meta| (*operator, meta.url.clone()))
                 } else {
                     None
                 }
             }

1986-2025: Consider extracting repeated pattern into a helper.

The same pattern for handling zero values in genesis/epoch boundary blocks is repeated three times (epoch_slot, slot, height). While the current code is correct and readable, a helper function could reduce duplication.

♻️ Optional helper extraction
fn genesis_aware_value(&self, value: i32) -> Option<i32> {
    match value {
        0 if matches!(
            self.block,
            MultiEraBlock::EpochBoundary(_) | MultiEraBlock::Byron(_)
        ) => None,
        x => Some(x),
    }
}

Then use: epoch_slot: epoch_slot.map(|x| x as i32).and_then(|x| self.genesis_aware_value(x))

Comment on lines +228 to +236
let to = from.saturating_add(pagination.count);
let genesis_index = curr_number;
let genesis_in_range = from <= genesis_index && genesis_index < to;

if genesis_in_range {
if let Some(genesis) = block_0.take() {
output.push(genesis);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix off‑by‑one when deciding whether to include genesis in previous pagination.
genesis_index should be curr_number - 1 (zero‑based offsets for “previous” list). Current logic skips genesis for ranges like from=5, count=5 when it should be included.

Suggested fix
-        let genesis_index = curr_number;
-        let genesis_in_range = from <= genesis_index && genesis_index < to;
+        let genesis_index = curr_number.saturating_sub(1);
+        let genesis_in_range =
+            curr_number > 0 && from <= genesis_index && genesis_index < to;
🤖 Prompt for AI Agents
In `@crates/minibf/src/routes/blocks.rs` around lines 228 - 236, The code
determines whether to include genesis in the "previous" pagination using
genesis_index = curr_number but that is off-by-one; change genesis_index to
curr_number.saturating_sub(1) (or otherwise subtract 1 safely) so the zero-based
previous range check (from, to = from.saturating_add(pagination.count))
correctly sets genesis_in_range and allows the block_0.take() ->
output.push(genesis) path to run for ranges that should include genesis; update
the genesis_index computation where it is declared so genesis_in_range logic and
use of block_0/output remain correct.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@crates/minibf/src/mapping.rs`:
- Around line 653-656: Remove the temporary debug print so stake registration
processing doesn't spam stderr: delete the dbg!(&reg) call inside the loop that
handles certs (the branch using pallas_extras::cert_as_stake_registration and
creating key via minicbor::to_vec(&reg)). If you still need visibility, replace
it with a proper logger call (e.g., tracing::debug!) but do not leave dbg!(&reg)
in the final code.
- Around line 653-699: Remove the dbg!(&reg) call and for stake and DRep
registration handling replicate the pool-registration on-chain check: before
adding key_deposit or reg.deposit consult
facade.read_cardano_entity::<AccountState> (for stake registrations using
reg/cred key) and facade.read_cardano_entity::<DRepState> (for DRep
registrations using reg.cred) and, like the PoolState branch in the loop, only
add the deposit when the on-chain state indicates the registration isn't already
current (perform the same self.block.slot() vs existing.register_slot comparison
and match Some/None as done for PoolState); keep using the repeated set for
intra-transaction deduplication.

Comment on lines +653 to +656
for cert in self.tx()?.certs() {
if let Some(reg) = pallas_extras::cert_as_stake_registration(&cert) {
dbg!(&reg);
let key = minicbor::to_vec(&reg).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove debug macro before merge.

dbg!(&reg) will spam stderr on every stake registration cert processed.

🐛 Suggested fix
-                dbg!(&reg);
🤖 Prompt for AI Agents
In `@crates/minibf/src/mapping.rs` around lines 653 - 656, Remove the temporary
debug print so stake registration processing doesn't spam stderr: delete the
dbg!(&reg) call inside the loop that handles certs (the branch using
pallas_extras::cert_as_stake_registration and creating key via
minicbor::to_vec(&reg)). If you still need visibility, replace it with a proper
logger call (e.g., tracing::debug!) but do not leave dbg!(&reg) in the final
code.

Comment on lines +653 to +699
for cert in self.tx()?.certs() {
if let Some(reg) = pallas_extras::cert_as_stake_registration(&cert) {
dbg!(&reg);
let key = minicbor::to_vec(&reg).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if repeated.insert((0, key.clone())) {
out += key_deposit;
}
}

if let Some(reg) = pallas_extras::cert_as_pool_registration(&cert) {
if repeated.insert((1, reg.operator.to_vec())) {
match facade
.read_cardano_entity::<PoolState>(reg.operator.as_slice())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
Some(pool) => {
if self.block.slot() <= pool.register_slot {
out += pool_deposit
}
}
None => out += pool_deposit,
}
}
}

if let Some(reg) = pallas_extras::cert_as_drep_registration(&cert) {
let key =
minicbor::to_vec(&reg.cred).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if repeated.insert((2, key)) {
out += reg.deposit as i64;
}
}

if let Some(reg) = pallas_extras::cert_as_stake_deregistration(&cert) {
let key = minicbor::to_vec(&reg).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if repeated.insert((3, key.clone())) {
out -= key_deposit;
}
}

if let Some(reg) = pallas_extras::cert_as_drep_unregistration(&cert) {
let key =
minicbor::to_vec(&reg.cred).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if repeated.insert((4, key)) {
out -= reg.deposit as i64;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and locate related state types
find crates/minibf/src -type f -name "*.rs" | head -20

Repository: txpipe/dolos

Length of output: 754


🏁 Script executed:

# Search for AccountState and DRepState in the codebase
rg "AccountState|DRepState" --type rust -i

Repository: txpipe/dolos

Length of output: 17618


🏁 Script executed:

# Examine the mapping.rs file more closely around the suspicious code
wc -l crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 89


🏁 Script executed:

# Look for similar patterns with PoolState to understand the approach
rg "read_cardano_entity.*PoolState" --type rust -A 5 -B 5

Repository: txpipe/dolos

Length of output: 911


🏁 Script executed:

# Check the dbg! macro usage and code maturity
rg "dbg!" crates/minibf/src/mapping.rs -n

Repository: txpipe/dolos

Length of output: 87


🏁 Script executed:

# Search for re-registration related comments or patterns
rg "re.*registr|duplicate.*registr|repeat.*registr" --type rust -i

Repository: txpipe/dolos

Length of output: 6785


🏁 Script executed:

# Read lines 640-710 to get full context
sed -n '640,710p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 2736


🏁 Script executed:

# Find the function signature and understand the facade type
sed -n '600,660p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 2103


🏁 Script executed:

# Check the governance route to see how DRepState is queried
sed -n '1,100p' crates/minibf/src/routes/governance.rs

Repository: txpipe/dolos

Length of output: 3209


🏁 Script executed:

# Search for how the facade is used in the txs route for comparison
grep -A 10 "read_cardano_entity" crates/minibf/src/routes/txs.rs | head -40

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Look at the governance route to see DRepState read pattern
sed -n '80,150p' crates/minibf/src/routes/governance.rs

Repository: txpipe/dolos

Length of output: 2075


🏁 Script executed:

# Search for re-registration or double registration patterns in Cardano code
rg "re.*register|double.*register|already.*register" crates/cardano --type rust -i -B 2 -A 2

Repository: txpipe/dolos

Length of output: 8327


🏁 Script executed:

# Check if there are any comments about re-registration in roll/accounts.rs
sed -n '1,100p' crates/cardano/src/roll/accounts.rs

Repository: txpipe/dolos

Length of output: 2983


🏁 Script executed:

# Look at StakeRegistration and StakeDeregistration implementations
rg "impl.*StakeRegistration|impl.*StakeDeregistration" crates/cardano/src/roll/accounts.rs -A 20

Repository: txpipe/dolos

Length of output: 2302


🏁 Script executed:

# Check if there are any comments about re-registration behavior or Cardano rules
rg "re.*register|cannot.*register.*twice" crates/cardano --type rust -i -B 3 -A 3

Repository: txpipe/dolos

Length of output: 10727


🏁 Script executed:

# Look at the Cardano ledger validation to understand if re-registrations are caught
rg "already.*registered|duplicate.*registration" crates/cardano --type rust -i -B 2 -A 2

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Check how compute_deposit is used - is it validating or computing estimates?
rg "compute_deposit" crates/minibf/src --type rust -B 5 -A 2

Repository: txpipe/dolos

Length of output: 939


🏁 Script executed:

# Look at the full function to see if there's a reason to only check pools
sed -n '590,610p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 875


🏁 Script executed:

# Check what PoolState.register_slot tracks to understand the pool check logic
rg "register_slot" crates/cardano/src --type rust -B 2 -A 2

Repository: txpipe/dolos

Length of output: 996


🏁 Script executed:

# Search for validation of certificate duplicates
rg "cert.*duplicate|certificate.*duplicate" crates/cardano --type rust -i -B 2 -A 2

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Check if there are ledger validation rules in the codebase
rg "validate.*cert|cert.*valid" crates/cardano/src/validate.rs --type rust -B 2 -A 2

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Look for what the repeated set is actually guarding against
sed -n '650,700p' crates/minibf/src/mapping.rs | cat -n

Repository: txpipe/dolos

Length of output: 2527


🏁 Script executed:

# Search for Cardano ledger specification comments about re-registrations
rg "re.*register|duplicate.*register|multiple.*register" crates --type rust -i

Repository: txpipe/dolos

Length of output: 2904


🏁 Script executed:

# Check if AccountState has a registered_at field and what it tracks
rg "registered_at|deregistered_at" crates/cardano/src/model.rs -B 1 -A 1

Repository: txpipe/dolos

Length of output: 697


🏁 Script executed:

# Check the pool registration check more carefully - what does register_slot mean
sed -n '620,680p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 2283


🏁 Script executed:

# Look at how register_slot is set when a pool is updated/re-registered
rg "PoolRegistration" crates/cardano/src/roll/pools.rs -B 3 -A 15

Repository: txpipe/dolos

Length of output: 2475


🏁 Script executed:

# Check if a stake account can be re-registered according to Cardano rules
rg "StakeRegistration" crates/cardano/src/roll/accounts.rs -B 5 -A 25 | head -80

Repository: txpipe/dolos

Length of output: 2689


🏁 Script executed:

# Verify whether is_registered() method checks both registered_at and deregistered_at
sed -n '1,50p' crates/cardano/src/model.rs | grep -A 10 "is_registered"

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Search for any ledger validation that prevents double registration
rg "cannot.*register|already.*registered" --type rust -i

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Check the Cardano spec or comments about re-registration rules
rg "re.*register.*allowed|re.*register.*denied|re.*register.*rule" crates --type rust -i

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Verify the exact logic of pool register_slot check - when would it NOT charge deposit
sed -n '668,680p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 588


🏁 Script executed:

# Look for how AccountState is used in the facade/domain to understand if it's queried
rg "read_cardano_entity.*AccountState" crates/minibf/src --type rust -B 2 -A 5

Repository: txpipe/dolos

Length of output: 598


🏁 Script executed:

# Check if there are any tests for compute_deposit that show expected behavior
fd "test" crates/minibf/src --type f | xargs grep -l "compute_deposit" 2>/dev/null

Repository: txpipe/dolos

Length of output: 40


🏁 Script executed:

# Look for any Cardano ledger documentation references in code
find crates -name "*.rs" -type f | xargs grep -l "ledger.*rule\|ledger.*spec" 2>/dev/null | head -5

Repository: txpipe/dolos

Length of output: 114


🏁 Script executed:

# Check if there's a spec URL or reference
rg "cardano.*spec|ledger.*rule|protocol.*rule" crates --type rust -i | head -20

Repository: txpipe/dolos

Length of output: 1525


🏁 Script executed:

# Look at the actual test case usage of compute_deposit in the repo
find . -name "*.rs" -type f -exec grep -l "compute_deposit" {} \; | head -10

Repository: txpipe/dolos

Length of output: 120


🏁 Script executed:

# Check the pallas library usage to understand validation
rg "pallas.*validate|validate.*cert" crates --type rust -i -B 1 -A 1 | head -30

Repository: txpipe/dolos

Length of output: 1571


🏁 Script executed:

# Search for any discussion about re-registration in comments
rg "re.*regist" crates --type rust -C 3 -i | grep -E "(TODO|FIXME|NOTE|re.*regist)" | head -20

Repository: txpipe/dolos

Length of output: 2236


🏁 Script executed:

# Check if there's a comment explaining the pool register_slot logic
sed -n '660,690p' crates/minibf/src/mapping.rs | grep -B 5 -A 10 "register_slot"

Repository: txpipe/dolos

Length of output: 702


🏁 Script executed:

# Look for any Cardano specification documentation or references in README or docs
find . -name "README*" -o -name "*.md" | xargs grep -i "re.*register\|deposit" 2>/dev/null | head -10

Repository: txpipe/dolos

Length of output: 780


🏁 Script executed:

# Check if the Cardano crate has any documentation about certificate validation
head -100 crates/cardano/src/lib.rs

Repository: txpipe/dolos

Length of output: 3117


🏁 Script executed:

# Check if there are integration tests that show expected behavior
find crates -name "*.rs" -type f -path "*/tests/*" -exec grep -l "compute_deposit\|registration" {} \;

Repository: txpipe/dolos

Length of output: 38


🏁 Script executed:

# Verify what the pool check is really guarding against
sed -n '665,680p' crates/minibf/src/mapping.rs

Repository: txpipe/dolos

Length of output: 766


🏁 Script executed:

# Look at the CHANGELOG to understand the history of this code
grep -B 3 -A 3 "deposit" CHANGELOG.md | head -40

Repository: txpipe/dolos

Length of output: 985


🏁 Script executed:

# Check if there's any explanation in the PR or commit that added this
git log --oneline --all --grep="deposit" 2>/dev/null | head -20

Repository: txpipe/dolos

Length of output: 290


🏁 Script executed:

# Verify the logic once more - what does the slot comparison mean?
# If current slot is BEFORE or EQUAL to register_slot, charge deposit
# If current slot is AFTER register_slot, don't charge (meaning pool was already registered)
echo "Pool logic analysis:"
echo "  If block.slot <= pool.register_slot: charge deposit"
echo "  If block.slot > pool.register_slot: DON'T charge (re-registration)"
echo ""
echo "Stake/DRep logic: only checks repeated set (same-tx duplicates), no on-chain state check"

Repository: txpipe/dolos

Length of output: 289


🏁 Script executed:

# Double-check the pool registration logic with more context
sed -n '665,680p' crates/minibf/src/mapping.rs | cat -n

Repository: txpipe/dolos

Length of output: 878


🏁 Script executed:

# Verify from the git commit what the actual fix was
git show e296c23 --stat 2>/dev/null || echo "Cannot access git"

Repository: txpipe/dolos

Length of output: 26657


🏁 Script executed:

# Check if there are any comments explaining the slot comparison logic
rg "register_slot" crates/minibf/src/mapping.rs -B 3 -A 3

Repository: txpipe/dolos

Length of output: 371


Remove dbg!() macro and add on-chain state checks for stake/DRep registrations.

Pool registrations check PoolState.register_slot to avoid recharging deposits on re-registration, but stake/DRep registrations don't check AccountState or DRepState. The repeated set only prevents duplicates within the current transaction. To handle re-registrations across transactions, query the on-chain state before adding deposits, similar to the pool registration pattern.

🤖 Prompt for AI Agents
In `@crates/minibf/src/mapping.rs` around lines 653 - 699, Remove the dbg!(&reg)
call and for stake and DRep registration handling replicate the
pool-registration on-chain check: before adding key_deposit or reg.deposit
consult facade.read_cardano_entity::<AccountState> (for stake registrations
using reg/cred key) and facade.read_cardano_entity::<DRepState> (for DRep
registrations using reg.cred) and, like the PoolState branch in the loop, only
add the deposit when the on-chain state indicates the registration isn't already
current (perform the same self.block.slot() vs existing.register_slot comparison
and match Some/None as done for PoolState); keep using the repeated set for
intra-transaction deduplication.

@scarmuega
scarmuega merged commit c751c60 into main Feb 3, 2026
10 of 11 checks passed
@scarmuega
scarmuega deleted the fix/improve-conformance-tests-coverage branch February 3, 2026 23:21
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