Skip to content

feat: introduce balius-proto for runtime <-> WASM schema#98

Open
scarmuega wants to merge 8 commits into
mainfrom
feat/balius-proto-schema
Open

feat: introduce balius-proto for runtime <-> WASM schema#98
scarmuega wants to merge 8 commits into
mainfrom
feat/balius-proto-schema

Conversation

@scarmuega
Copy link
Copy Markdown
Member

@scarmuega scarmuega commented May 1, 2026

Summary

  • Introduces balius-proto, a Balius-owned protobuf schema (mirrors utxorpc-spec 0.17.0 wire format) that defines the WASM ABI independently of upstream u5c.
  • Runtime now ingests upstream u5c types and converts once into balius_proto::cardano::* at chainsync ingress and at ledger reads; BigInt → u64/i64 is fallible and halts on overflow.
  • balius-sdk drops the utxorpc-spec dep in favor of balius-proto; Utxo<D>::coin() and PParams access become plain integer reads (no BigInt unwrap).
  • PParams is handled separately: it crosses WIT as JSON, not protobuf. The runtime emits the exact utxorpc-spec 0.17 pbjson shape via an imperative converter (pparams_to_legacy_json) — workers built against pre-BigInt SDKs decode the bytes unchanged with their own pbjson decoder.
  • balius-proto stays pure schema. All u5c → balius_proto conversion (Tx/UTxO/PParams) lives in balius-runtime/src/ledgers/u5c.rs so the schema crate has no coupling to any upstream service.

Why

The runtime was shipping raw utxorpc::spec::cardano::* prost bytes across the WIT boundary, which implicitly made the upstream u5c spec the WASM ABI. When u5c 0.17 → 0.18.1 flipped TxOutput.coin (and similar fields across Asset, Tx.fee, Collateral, certs, governance actions, PParams) from uint64 to BigInt, every existing pre-BigInt worker started decoding garbage. This PR breaks that coupling: workers built against the old SDK keep working because the new bytes are byte-identical to what they expect, and future upstream u5c churn is absorbed by a single hardcoded conversion layer inside the runtime — not inside the schema crate.

Architecture

  • balius-proto owns the wire schema (cardano.rs) and a wire-format invariant test (wire_compat_*: balius_proto bytes decode as utxorpc-spec 0.17 prost). No upstream deps.
  • balius-runtime/src/ledgers/u5c.rs owns u5c → balius_proto conversion (convert_tx, convert_tx_output, convert_tx_input, convert_asset, convert_multiasset, pparams_to_legacy_json + helpers, ConvertError).
  • balius-runtime/tests/u5c_convert.rs asserts the end-to-end ABI contract: bytes/JSON produced by the conversion functions decode under the actual utxorpc-spec 0.17 decoder.

Notes

  • Tx/UTxO/etc. travel as prost bytes — handled by mirroring 0.17 prost tags in balius_proto::cardano::* so old workers decode unchanged. Tags absent from balius_proto (script, certs, withdrawals, mint, reference_inputs, validity, collateral, proposals, ...) are deliberately dropped — workers needing them must re-add them here and update the converter.
  • PParams travels as JSON. Mirror-structs buy nothing for it (workers don't decode it via prost), so it gets its own imperative converter that emits exact 0.17 pbjson conventions: camelCase keys, u64/i64 as JSON strings, u32/i32 as plain numbers, None / proto3-default fields omitted. All 31 upstream fields are emitted, so old workers that read any field — not just coinsPerUtxoByte — keep working.
  • Conversion error semantics: BigUInt/BigNInt and out-of-range Int produce ConvertError::Overflow. Surfaces as Error::Convert in the runtime and wit::LedgerError::Upstream for ledger paths — workers halt rather than receive lossy values.
  • balius-proto ships pre-generated cardano.rs (no protoc required at build); its only dep is prost + bytes. Tests have one dev-dep (utxorpc-spec 0.17) used solely to assert wire-format invariance.
  • New SDK PParams is minimal (coins_per_utxo_byte only — the field actually consumed by MinUtxoLovelace) but declared with serde_with::DisplayFromStr + rename = "coinsPerUtxoByte" so it parses the same pbjson shape old workers expect. Add fields here as needed; serde ignores unknown ones the runtime emits.
  • mock_pparams.json is the canonical 0.17 pbjson snapshot, so the mock ledger and the real ledger hand workers identical bytes.

Test plan

  • cargo check --workspace clean.
  • cargo test -p balius-proto — 2/2 wire-format invariant tests (schema in, 0.17 prost out).
  • cargo test -p balius-runtime --test u5c_convert — 8/8:
    • convert_pparams_decodes_under_017_pbjson — load-bearing PParams ABI assertion (converted JSON deserializes under real 0.17 pbjson decoder).
    • convert_then_decode_with_017 — load-bearing Tx/UTxO ABI assertion (converted bytes decode under real 0.17 prost decoder).
    • 6 covering overflow paths, BigInt flattening, omit-default behavior.
  • cargo test --workspace — all pass except the pre-existing u5c-chainsync::wallet_balance (5-vs-7 WIT variant mismatch from a stale checked-in wallet.wasm; verified failing identically on main).
  • e2e::faucet_claim exercises the full PParams path end-to-end (mock ledger ships mock_pparams.json → SDK PParams deserializes → MinUtxoLovelace reads coins_per_utxo_byte → tx build succeeds).
  • Smoke test against a staging u5c endpoint with an existing pre-BigInt worker to confirm utxo_handled / tx_handled metrics advance and no decode errors appear in logs.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced balius-core library providing versioned Cardano protocol definitions with improved wire compatibility.
  • Breaking Changes

    • Block::txs now returns Result<Vec<Tx>, Error> instead of Vec<Tx> (transaction conversion may fail).
    • Updated Cardano transaction and output types throughout runtime and SDK.
    • Simplified protocol parameter handling with new direct field access.

Review Change Stack

Decouple the WASM ABI from the upstream utxorpc spec. The runtime
previously shipped utxorpc::spec::cardano::* prost-encoded bytes across
the WIT boundary as cbor; that implicitly made the upstream u5c spec the
WASM ABI, so the 0.17 -> 0.18 BigInt break propagated to every existing
worker.

balius-proto owns a frozen schema (mirrors utxorpc-spec 0.17.0 wire
format). The runtime converts upstream u5c types into this schema once
at chainsync ingress and at ledger reads. BigInt -> u64/i64 is fallible
and halts the worker on overflow. Workers built against the pre-BigInt
SDK keep decoding cleanly because the new bytes are wire-identical to
what they expect.

balius-sdk drops the utxorpc-spec dep in favor of balius-proto;
Utxo<D>::coin() and PParams field access become plain integers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4abf086-8451-4378-916e-390bceb438b8

📥 Commits

Reviewing files that changed from the base of the PR and between cb0ff36 and fc71fb8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • Cargo.toml
  • balius-core/Cargo.toml
  • balius-core/src/lib.rs
  • balius-core/src/proto/mod.rs
  • balius-core/src/proto/v0/cardano.rs
  • balius-core/src/proto/v0/mod.rs
  • balius-core/tests/wire_compat.rs
  • balius-runtime/Cargo.toml
  • balius-runtime/src/ledgers/u5c/convert.rs
  • balius-runtime/src/ledgers/u5c/mod.rs
  • balius-runtime/src/lib.rs
  • balius-sdk/Cargo.toml
  • balius-sdk/src/qol.rs
  • examples/asteria-tracker/Cargo.toml
  • examples/asteria-tracker/src/lib.rs
✅ Files skipped from review due to trivial changes (4)
  • balius-core/src/proto/v0/mod.rs
  • balius-core/src/proto/mod.rs
  • balius-core/src/lib.rs
  • balius-core/src/proto/v0/cardano.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • balius-sdk/Cargo.toml
  • examples/asteria-tracker/src/lib.rs
  • balius-runtime/src/ledgers/u5c/mod.rs
  • balius-runtime/src/lib.rs
  • balius-runtime/src/ledgers/u5c/convert.rs

📝 Walkthrough

Walkthrough

This PR establishes a new balius-core crate containing Cardano protobuf schemas wire-compatible with utxorpc-spec v0.17, implements a conversion layer to translate from upstream v0.18 types, validates round-trip compatibility, and integrates conversions throughout the runtime and SDK to replace external protobuf dependencies.

Changes

Protobuf migration and integration

Layer / File(s) Summary
balius-core crate foundation and message schema
Cargo.toml, balius-core/Cargo.toml, balius-core/src/lib.rs, balius-core/src/proto/mod.rs, balius-core/src/proto/v0/mod.rs, balius-core/src/proto/v0/cardano.rs
Workspace adds balius-core member. New balius-core crate publishes complete Cardano protobuf message hierarchy via Prost: TxInput, TxOutput, Datum, Tx, WitnessSet, Asset, Multiasset, VKeyWitness, NativeScript, BigInt, PlutusData, and Script with nested oneof enums for script types and data constructors.
Wire compatibility tests for legacy schema
balius-core/tests/wire_compat.rs
Test suite encodes legacy TxOutput and Tx values to bytes, decodes them via utxorpc-spec v0.17 types, and asserts round-trip correctness while verifying omitted protobuf tags decode as defaults.
Runtime conversion infrastructure and error types
balius-runtime/Cargo.toml, balius-runtime/src/ledgers/u5c/convert.rs
balius-runtime adds balius-core and utxorpc-spec_017 (dev) as dependencies. Conversion module establishes type aliases and defines ConvertError with Overflow and Decode variants. Provides helpers: unwrap_u64 for safe BigInt-to-u64 extraction, try_map for fallible vectors, and roundtrip/roundtrip_opt for prost encode/decode.
Protobuf message and parameter converters
balius-runtime/src/ledgers/u5c/convert.rs
Implements convert_asset, convert_multiasset, convert_tx_output, convert_tx_input, and convert_tx fallible converters using internal helpers. Implements pparams_to_legacy_json to construct utxorpc-spec v0.17 pbjson JSON (camelCase, conditional omission of defaults, BigInt flattening). Includes unit tests for prost/pbjson round-trip and overflow detection.
Runtime ledger conversion helpers and type migration
balius-runtime/src/lib.rs, balius-runtime/src/ledgers/u5c/mod.rs
u5c module exposes convert submodule and adds chain_utxo_to_wit and utxo_page_to_wit helpers for converting ledger results with error propagation. Updates Ledger::read_utxos, search_utxos, and read_params to use helpers. Runtime adds Error::Convert variant and updates TxInput::Cardano, Utxo::Cardano, Tx::Cardano to use balius_core types. Block::txs becomes Result&lt;Vec&lt;Tx&gt;, Error&gt; with per-transaction conversion via convert_tx. Updates apply_block and undo_block to propagate conversion errors.
SDK dependencies and type simplifications
balius-sdk/Cargo.toml, balius-sdk/src/txbuilder/mod.rs, balius-sdk/src/qol.rs, balius-sdk/src/txbuilder/dsl.rs
balius-sdk replaces utxorpc-spec with balius-core path dependency. Defines local PParams struct with coins_per_utxo_byte: u64, deserializing coinsPerUtxoByte from JSON string via serde_with::DisplayFromStr. Updates Utxo&lt;D&gt; and Tx wrappers to use balius_core types and simplifies Utxo::coin() and MinUtxoLovelace::eval to access coin/pparams fields directly.
Example dependency and import updates
examples/asteria-tracker/Cargo.toml, examples/asteria-tracker/src/lib.rs
Example switches to local balius-sdk and balius-core path dependencies, removes utxorpc-spec. Updates imports of PlutusData, big_int, plutus_data to source from balius_core::proto::v0::cardano.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • txpipe/balius#76: Both PRs modify balius-runtime/src/lib.rs's public Error type (adding conversion-specific error variants) and may conflict in the same enum/integration points.

Poem

🐰 A proto crate blooms, with schemas clean and bright,
Conversion helpers guide the v0.18 to v0.17 flight,
Wire tests verify each byte roundtrips just right,
Runtime, SDK, and examples—all in sight,
Legacy shapes dance in Balius light! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: introducing a new schema crate (balius-proto/balius-core) for the WASM runtime boundary, which is the primary focus of the changeset.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/balius-proto-schema

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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)
balius-runtime/src/lib.rs (1)

315-319: 💤 Low value

Consider making from_bytes fallible for robustness.

The unwrap() on Block::decode will panic if the stored bytes are malformed. While blocks are converted at chain-sync ingress (so stored data should be valid), returning Result would provide graceful handling for edge cases like store corruption.

♻️ Optional: Return Result instead of panicking
-    pub fn from_bytes(data: &[u8]) -> Self {
+    pub fn from_bytes(data: &[u8]) -> Result<Self, prost::DecodeError> {
         use prost::Message;
 
-        Self::Cardano(balius_proto::cardano::Block::decode(data).unwrap())
+        Ok(Self::Cardano(balius_proto::cardano::Block::decode(data)?))
     }

This would require updating call sites to handle the Result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@balius-runtime/src/lib.rs` around lines 315 - 319, Change from_bytes to be
fallible: replace the current panic-on-error behavior in from_bytes (which
constructs Self::Cardano from
balius_proto::cardano::Block::decode(data).unwrap()) with a Result-returning
signature (e.g., Result<Self, prost::DecodeError> or your crate error type) and
propagate the decode error using ? or map_err so malformed bytes are returned as
Err; update callers of from_bytes to handle the Result accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@balius-runtime/src/lib.rs`:
- Around line 315-319: Change from_bytes to be fallible: replace the current
panic-on-error behavior in from_bytes (which constructs Self::Cardano from
balius_proto::cardano::Block::decode(data).unwrap()) with a Result-returning
signature (e.g., Result<Self, prost::DecodeError> or your crate error type) and
propagate the decode error using ? or map_err so malformed bytes are returned as
Err; update callers of from_bytes to handle the Result accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80355eac-cee7-4a23-82f6-a522fe0c6b2f

📥 Commits

Reviewing files that changed from the base of the PR and between e9c8cd5 and a2cdca5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • balius-proto/Cargo.toml
  • balius-proto/src/cardano.rs
  • balius-proto/src/cardano.serde.rs
  • balius-proto/src/convert.rs
  • balius-proto/src/lib.rs
  • balius-proto/tests/wire_compat.rs
  • balius-runtime/Cargo.toml
  • balius-runtime/src/drivers/chainsync.rs
  • balius-runtime/src/ledgers/u5c.rs
  • balius-runtime/src/lib.rs
  • balius-sdk/Cargo.toml
  • balius-sdk/src/qol.rs
  • balius-sdk/src/txbuilder/dsl.rs
  • balius-sdk/src/txbuilder/mod.rs
  • examples/asteria-tracker/Cargo.toml
  • examples/asteria-tracker/src/lib.rs

scarmuega and others added 2 commits May 1, 2026 10:28
Drop everything from cardano.rs except the wire surface workers
actually consume:

  TxOutput { address, coin, assets }
  Tx       { inputs, outputs, fee, hash }
  TxInput  { tx_hash, output_index, as_output }
  Multiasset, Asset (output_coin only)

Tags absent from these structs (datum, script, certificates,
withdrawals, mint, witnesses, validity, auxiliary, proposals, ...) are
deliberately dropped — workers needing them must extend the schema and
the converter explicitly. This shrinks balius-proto from ~5400 LOC of
generated cardano + 14k LOC of pbjson serde to 181 LOC total.

Block/BlockBody/BlockHeader move out of balius-proto since they never
cross WIT. The runtime's Block enum reverts to wrapping
utxorpc::spec::cardano::Block; per-Tx conversion happens at apply_block
/ undo_block via the fallible Block::txs() accessor.

PParams gone too: the SDK txbuilder now defines a minimal
PParams { coins_per_utxo_byte: u64 } locally, and the runtime's
read-params emits a matching minimal JSON shape.

asteria-tracker example trimmed to use only multiasset fields (datum-
derived position is dropped since the datum tag is gone).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The datum payload (PlutusData) and the witness set are common worker
inputs — bring them back into the trimmed schema. Both were
wire-stable across utxorpc-spec 0.17 -> 0.18, so the converter just
roundtrips them via prost.

New types in cardano.rs: Datum, PlutusData (+ Constr, PlutusDataPair,
PlutusDataMap, PlutusDataArray, BigInt), Script (+ NativeScript,
NativeScriptList, ScriptNOfK), WitnessSet, VKeyWitness.

Tags on PlutusData and Constr.any_constructor match upstream 0.17
verbatim (verified by failing-then-fixing wire_compat tests).

asteria-tracker example restores its datum-derived position tracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@SupernaviX SupernaviX left a comment

Choose a reason for hiding this comment

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

Hopefully tx3 integration lets you throw out all this compat stuff

scarmuega and others added 2 commits May 1, 2026 13:36
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ompat

Pre-BigInt workers ship a utxorpc-spec 0.17 pbjson deserializer for the
JSON `read-params` returns. The prior approach in this PR — emitting
`{"coins_per_utxo_byte": N}` snake_case — broke that contract: the key
doesn't match, the value type doesn't match, and 30 other fields the
worker may read are gone entirely.

PParams crosses the WIT boundary as JSON, not protobuf, so prost
wire-compat (the trick that anchors Tx/UTxO bytes) buys nothing for it.
Instead, `pparams_to_legacy_json` walks the upstream `PParams`
imperatively and emits exactly the JSON shape utxorpc-spec 0.17's pbjson
serializer would produce: camelCase keys, u64/i64 as JSON strings,
u32/i32 as plain numbers, None / proto3-default fields omitted. BigInt
fields collapse via `unwrap_u64`; overflow halts the worker as before.

The SDK side keeps a minimal `PParams` (only `coins_per_utxo_byte` is
actually consumed today) but now declares it with
`serde_with::DisplayFromStr` and `rename = "coinsPerUtxoByte"` so it
reads the same pbjson shape as old workers' decoder.

`mock_pparams.json` is restored to the canonical 0.17 pbjson snapshot,
so the mock ledger and the real ledger now hand workers identical bytes.

Tests:
- `convert_pparams_decodes_under_017_pbjson` — load-bearing: converted
  PParams round-trips through utxorpc-spec 0.17's actual pbjson decoder.
- `convert_pparams_overflow_propagates` — BigUInt halts.
- `convert_pparams_omits_default_fields` — pbjson omit-default preserved.
- `e2e::faucet_claim` exercises the full path end-to-end (mock JSON →
  SDK PParams → MinUtxoLovelace → tx build) and passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Actionable comments posted: 1

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

Inline comments:
In `@balius-proto/src/convert.rs`:
- Around line 38-43: The unwrap_u64 function currently only accepts
upstream::big_int::BigInt::Int and treats BigUInt as Overflow; update unwrap_u64
to also handle upstream::big_int::BigInt::BigUInt by decoding its byte
representation into a u64 (attempt a checked-from-bytes or bigint-to-u64
conversion) and return Ok(u64) when it fits, otherwise return
ConvertError::Overflow; retain the existing Int branch but ensure negative Ints
still map to ConvertError::Overflow and positive Ints convert to u64 as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 327f5355-3225-4b03-8465-462021065b3f

📥 Commits

Reviewing files that changed from the base of the PR and between 3b6926d and 8975462.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • balius-proto/Cargo.toml
  • balius-proto/src/convert.rs
  • balius-proto/tests/wire_compat.rs
  • balius-runtime/src/ledgers/u5c.rs
  • balius-sdk/src/txbuilder/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • balius-proto/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • balius-sdk/src/txbuilder/mod.rs
  • balius-runtime/src/ledgers/u5c.rs

Comment thread balius-proto/src/convert.rs Outdated
Comment on lines +38 to +43
fn unwrap_u64(b: Option<&upstream::BigInt>) -> Result<u64, ConvertError> {
match b.and_then(|x| x.big_int.as_ref()) {
None => Ok(0),
Some(upstream::big_int::BigInt::Int(v)) if *v >= 0 => Ok(*v as u64),
_ => Err(ConvertError::Overflow),
}
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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In utxorpc-spec 0.18.1, how is utxorpc::v1alpha::cardano::big_int::BigIntencoded, and are positive values that do not fitInt(i64)expected to use theBigUInt variant?

💡 Result:

In utxorpc-spec 0.18.1, the utxorpc::v1alpha::cardano::big_int::BigInt message is encoded as a oneof field in Protocol Buffers, allowing it to represent integers of varying sizes and signs [1]. The BigInt structure typically includes the following variants to handle different integer ranges [1]: 1. int: Used for values that fit within a standard 64-bit signed integer (int64) [1]. 2. big_uint: Used for positive values that exceed the range of a 64-bit signed integer [1]. This variant is expected to store the value as raw bytes (typically representing an unsigned big integer) [1]. 3. big_n_int: Used for negative values that fall outside the range of a 64-bit signed integer [1]. Yes, positive values that do not fit into a standard int64 are expected to use the big_uint variant [1]. When encoding such values, the integer is converted into its raw byte representation (often using big-endian encoding) and stored within the big_uint field [1]. Conversely, when decoding, the system checks which variant is present and reconstructs the integer accordingly [1].

Citations:


🏁 Script executed:

# First, check if the file exists and view the function in context
find . -name "convert.rs" -type f | head -5

Repository: txpipe/balius

Length of output: 87


🏁 Script executed:

# View the convert.rs file to understand the implementation
cat -n ./balius-proto/src/convert.rs | head -80

Repository: txpipe/balius

Length of output: 3253


🏁 Script executed:

# View more of the convert.rs file to see if there are other BigInt handling functions
cat -n ./balius-proto/src/convert.rs | sed -n '38,150p'

Repository: txpipe/balius

Length of output: 4516


🏁 Script executed:

# Search for the BigInt protobuf definition or imports to understand the structure
rg "big_int|BigInt" --type rs -A 3 -B 3 | head -100

Repository: txpipe/balius

Length of output: 84


🏁 Script executed:

# Search for BigInt references in the codebase
rg "big_int|BigInt" -A 3 -B 3 | head -150

Repository: txpipe/balius

Length of output: 9974


🏁 Script executed:

# Check the complete BigInt enum definition in cardano.rs
cat -n ./balius-proto/src/cardano.rs | grep -A 20 "pub enum BigInt"

Repository: txpipe/balius

Length of output: 841


🏁 Script executed:

# Look at the tests to understand the expected behavior
cat -n ./balius-proto/tests/wire_compat.rs | grep -B 5 -A 15 "convert_overflow_big_uint_errors"

Repository: txpipe/balius

Length of output: 856


🏁 Script executed:

# Check if there's any handling of BigUInt elsewhere in convert.rs to understand complete scope
cat -n ./balius-proto/src/convert.rs | wc -l

Repository: txpipe/balius

Length of output: 61


🏁 Script executed:

# Check if there are other BigInt/BigUInt handling patterns in convert.rs
cat -n ./balius-proto/src/convert.rs | grep -i "bigint\|bigunt" -B 2 -A 2

Repository: txpipe/balius

Length of output: 1877


🏁 Script executed:

# Check how many places unwrap_u64 is called to understand the impact scope
rg "unwrap_u64" balius-proto/src/convert.rs

Repository: txpipe/balius

Length of output: 837


Decode BigUInt values that fit within u64 instead of unconditionally treating them as overflow.

Line 41 only accepts Int(v), so a positive upstream BigUInt currently always becomes ConvertError::Overflow even when the value fits the u64 target. This blocks valid conversions for the i64::MAX + 1 ..= u64::MAX range, turning legitimate data into worker-stopping errors across multiple critical fields (coins, fees, protocol parameters).

The unwrap_u64 function needs to:

  • Decode BigUInt bytes to u64 when they represent values in that range
  • Keep Overflow only for truly out-of-range values and negative integers
🤖 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 `@balius-proto/src/convert.rs` around lines 38 - 43, The unwrap_u64 function
currently only accepts upstream::big_int::BigInt::Int and treats BigUInt as
Overflow; update unwrap_u64 to also handle upstream::big_int::BigInt::BigUInt by
decoding its byte representation into a u64 (attempt a checked-from-bytes or
bigint-to-u64 conversion) and return Ok(u64) when it fits, otherwise return
ConvertError::Overflow; retain the existing Int branch but ensure negative Ints
still map to ConvertError::Overflow and positive Ints convert to u64 as before.

scarmuega and others added 2 commits May 15, 2026 10:14
…apter

balius-proto is the Balius-owned wire schema crate; coupling it to an
arbitrary upstream service (utxorpc-spec) defeats the point of having a
schema crate in the first place. The conversion is u5c-specific and only
consumed by balius-runtime, so it moves into the u5c ledger adapter.

balius-proto/src/convert.rs is deleted along with its `convert` feature
and the `utxorpc-spec` / `serde_json` deps. The crate is now pure types
plus wire-format invariant assertions.

The TryFrom impls become plain `convert_tx` / `convert_tx_output` / etc.
functions inside `balius-runtime/src/ledgers/u5c.rs` (the orphan rule
forbids the impls here — neither side is local to balius-runtime).
`ConvertError` and `pparams_to_legacy_json` move alongside them. Call
sites in `lib.rs::Block::txs` and the ledger paths update from
`.try_into()` to the named functions. No behavior change.

Conversion tests move from `balius-proto/tests/wire_compat.rs` to a new
`balius-runtime/tests/u5c_convert.rs`. balius-proto keeps only the two
`wire_compat_*` tests — the wire-format invariant of the schema itself,
no upstream involved.

Verified: cargo check --workspace clean; 2/2 wire-compat tests in
balius-proto; 8/8 u5c convert tests in balius-runtime; e2e::faucet_claim
still passes (end-to-end ABI compat path through the mock ledger);
u5c-chainsync::wallet_balance still fails on the same pre-existing
stale-wasm 5-vs-7 WIT variant mismatch, unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The u5c ledger module grew large enough that the ledger-facing surface
(Config, Ledger, wit ↔ utxorpc From impls, chain_utxo_to_wit) was sharing
a file with the u5c → balius_proto conversion layer. Promote
`ledgers/u5c.rs` to a folder with two siblings:

  ledgers/u5c/
    mod.rs        — ledger adapter: Config, Ledger, pattern conversions
    convert.rs    — ConvertError, convert_tx / convert_tx_output / ...,
                    pparams_to_legacy_json, all the BigInt-flattening
                    helpers, and the unit tests that assert ABI compat
                    against utxorpc-spec 0.17 (prost + pbjson)

The previously integration-style tests at
`balius-runtime/tests/u5c_convert.rs` move into a `#[cfg(test)] mod tests`
inside `convert.rs` — they're testing internals of that module, so unit
tests are the right home. Test count unchanged; they now run as part of
the runtime's lib tests.

Call sites in `lib.rs` updated to the new path
(`ledgers::u5c::convert::convert_tx`, `ledgers::u5c::convert::ConvertError`).
No re-exports — the structure is explicit.

Verified: cargo check --workspace clean; 9/9 lib tests pass (8 convert
tests + 1 router test); e2e::faucet_claim still passes; pre-existing
u5c-chainsync::wallet_balance stale-wasm failure unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 (3)
balius-proto/tests/wire_compat.rs (1)

48-50: ⚡ Quick win

Assert decoded datum payload bytes to complete the TxOutput wire-compat invariant.

Line 29-35 constructs datum.payload, but the test never checks it post-decode. Adding that assertion would catch regressions where hash/CBOR survive but payload mapping drifts.

Suggested test addition
     let datum = decoded.datum.as_ref().expect("datum present");
     assert_eq!(datum.hash.to_vec(), vec![0xEE; 32]);
     assert_eq!(datum.original_cbor.to_vec(), vec![0xFF; 8]);
+    let payload = datum.payload.as_ref().expect("datum payload present");
+    match payload.plutus_data.as_ref() {
+        Some(v17::plutus_data::PlutusData::BoundedBytes(bytes)) => {
+            assert_eq!(bytes.to_vec(), b"datum-payload".to_vec());
+        }
+        other => panic!("unexpected datum payload variant: {other:?}"),
+    }
     // Dropped TxOutput tags decode as defaults:
     assert!(decoded.script.is_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 `@balius-proto/tests/wire_compat.rs` around lines 48 - 50, The test currently
checks decoded.datum.hash and decoded.datum.original_cbor but omits asserting
the decoded datum.payload bytes; update the wire_compat.rs test to also assert
that decoded.datum.as_ref().expect("datum present").payload (or its byte
representation) equals the original payload constructed earlier (the bytes set
in datum.payload in lines 29-35) so the TxOutput wire-compat invariant covers
hash, original_cbor, and payload together.
balius-runtime/src/ledgers/u5c/convert.rs (2)

312-546: ⚡ Quick win

Test gap: datum and witnesses roundtrip is load-bearing but not exercised.

Lines 63–66 make the explicit assumption that Datum and WitnessSet share an identical wire format between utxorpc-spec 0.17 and 0.18, and the whole roundtrip_opt path depends on it. None of the current tests round-trip a non-None datum or witness set through convert_tx_output / convert_tx and then re-decode under v17 prost. If the assumption is ever invalidated (e.g., a new field added at a low tag with a non-default value, or a tag reused), the failure mode is silent garbled bytes / decode error in production rather than at test time.

A small extension of convert_then_decode_with_017 covering a populated datum and witness set would lock the invariant down.

🧪 Sketch of additional coverage
#[test]
fn convert_then_decode_with_017_datum_and_witnesses() {
    let upstream_out = v18::TxOutput {
        address: vec![0xAA; 28].into(),
        coin: Some(v18::BigInt { big_int: Some(v18::big_int::BigInt::Int(1_000_000)) }),
        assets: vec![],
        datum: Some(v18::Datum { /* populate a representative payload */ ..Default::default() }),
        script: None,
    };
    let bal = convert_tx_output(upstream_out).expect("convert");
    let decoded = v17::TxOutput::decode(bal.encode_to_vec().as_slice()).expect("0.17 decode");
    assert!(decoded.datum.is_some());

    let upstream_tx = v18::Tx {
        witnesses: Some(v18::WitnessSet { /* populate representative witnesses */ ..Default::default() }),
        hash: vec![0xCC; 32].into(),
        ..Default::default()
    };
    let bal_tx = convert_tx(upstream_tx).expect("convert");
    let decoded_tx = v17::Tx::decode(bal_tx.encode_to_vec().as_slice()).expect("0.17 decode");
    assert!(decoded_tx.witnesses.is_some());
}
🤖 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 `@balius-runtime/src/ledgers/u5c/convert.rs` around lines 312 - 546, Add tests
to exercise datum and witness set round-trips: extend the existing
convert_then_decode_with_017 coverage by adding a new test (e.g.,
convert_then_decode_with_017_datum_and_witnesses) that builds a v18::TxOutput
with a non-None datum and calls convert_tx_output, encodes the resulting
legacy::TxOutput and decodes it with v17::TxOutput::decode asserting
decoded.datum.is_some(); likewise build a v18::Tx with a non-None witnesses,
call convert_tx, encode the legacy::Tx and decode with v17::Tx::decode asserting
decoded_tx.witnesses.is_some(); ensure the datum and witness payloads are
representative (populate non-default fields) so the roundtrip via
convert_tx_output and convert_tx will catch wire-format mismatches.

229-307: ⚡ Quick win

u32 PParams fields are emitted as JSON strings, contradicting the documented pbjson convention.

The module documentation (lines 130–131) states the output follows pbjson rules: "u64/i64 emitted as JSON strings, u32/i32 as plain numbers". However, put_u64_str unconditionally emits as Value::String, and is used for fields that are uint32 in utxorpc-spec 0.17 — max_tx_size, max_block_body_size, max_block_header_size, pool_retirement_epoch_bound, desired_number_of_pools, max_value_size, collateral_percentage, max_collateral_inputs, committee_term_limit, governance_action_validity_period, drep_inactivity_period. Notably min_committee_size (also uint32) is correctly emitted as a plain number via Value::from, so the file is internally inconsistent.

The test passes because pbjson deserializers accept both strings and numbers for 32-bit ints, but the output is no longer byte-identical to what utxorpc-spec 0.17's canonical pbjson serializer would emit — violating the stated ABI guarantee.

Add a put_u32 helper and route u32 fields through it:

♻️ Proposed fix sketch
     let put_u64_str = |o: &mut serde_json::Map<String, Value>, k: &str, v: u64| {
         if v != 0 {
             o.insert(k.into(), Value::String(v.to_string()));
         }
     };
+    let put_u32 = |o: &mut serde_json::Map<String, Value>, k: &str, v: u32| {
+        if v != 0 {
+            o.insert(k.into(), Value::from(v));
+        }
+    };

     let cput = unwrap_u64(p.coins_per_utxo_byte.as_ref())?;
     put_u64_str(&mut o, "coinsPerUtxoByte", cput);
-    put_u64_str(&mut o, "maxTxSize", p.max_tx_size);
+    put_u32(&mut o, "maxTxSize", p.max_tx_size);
     let mfc = unwrap_u64(p.min_fee_coefficient.as_ref())?;
     put_u64_str(&mut o, "minFeeCoefficient", mfc);
     let mfcst = unwrap_u64(p.min_fee_constant.as_ref())?;
     put_u64_str(&mut o, "minFeeConstant", mfcst);
-    put_u64_str(&mut o, "maxBlockBodySize", p.max_block_body_size);
-    put_u64_str(&mut o, "maxBlockHeaderSize", p.max_block_header_size);
+    put_u32(&mut o, "maxBlockBodySize", p.max_block_body_size);
+    put_u32(&mut o, "maxBlockHeaderSize", p.max_block_header_size);
     // ...and similarly for the remaining u32 fields below.

Also fold min_committee_size into the same helper for consistency.

🤖 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 `@balius-runtime/src/ledgers/u5c/convert.rs` around lines 229 - 307, The code
currently uses put_u64_str to emit many uint32 PParams fields as JSON strings,
violating the pbjson convention; add a new helper put_u32 = |o: &mut
serde_json::Map<String, Value>, k: &str, v: u32| { if v != 0 {
o.insert(k.into(), Value::from(v)); } } and replace uses of put_u64_str for the
uint32 fields (max_tx_size, max_block_body_size, max_block_header_size,
pool_retirement_epoch_bound, desired_number_of_pools, max_value_size,
collateral_percentage, max_collateral_inputs, committee_term_limit,
governance_action_validity_period, drep_inactivity_period) to call put_u32
instead; also fold min_committee_size into this helper path for consistency and
leave unwrap_u64 and put_u64_str for true u64 fields like coins_per_utxo_byte,
min_fee_coefficient, stake_key_deposit, pool_deposit, min_pool_cost,
governance_action_deposit, drep_deposit.
🤖 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 `@balius-proto/tests/wire_compat.rs`:
- Around line 48-50: The test currently checks decoded.datum.hash and
decoded.datum.original_cbor but omits asserting the decoded datum.payload bytes;
update the wire_compat.rs test to also assert that
decoded.datum.as_ref().expect("datum present").payload (or its byte
representation) equals the original payload constructed earlier (the bytes set
in datum.payload in lines 29-35) so the TxOutput wire-compat invariant covers
hash, original_cbor, and payload together.

In `@balius-runtime/src/ledgers/u5c/convert.rs`:
- Around line 312-546: Add tests to exercise datum and witness set round-trips:
extend the existing convert_then_decode_with_017 coverage by adding a new test
(e.g., convert_then_decode_with_017_datum_and_witnesses) that builds a
v18::TxOutput with a non-None datum and calls convert_tx_output, encodes the
resulting legacy::TxOutput and decodes it with v17::TxOutput::decode asserting
decoded.datum.is_some(); likewise build a v18::Tx with a non-None witnesses,
call convert_tx, encode the legacy::Tx and decode with v17::Tx::decode asserting
decoded_tx.witnesses.is_some(); ensure the datum and witness payloads are
representative (populate non-default fields) so the roundtrip via
convert_tx_output and convert_tx will catch wire-format mismatches.
- Around line 229-307: The code currently uses put_u64_str to emit many uint32
PParams fields as JSON strings, violating the pbjson convention; add a new
helper put_u32 = |o: &mut serde_json::Map<String, Value>, k: &str, v: u32| { if
v != 0 { o.insert(k.into(), Value::from(v)); } } and replace uses of put_u64_str
for the uint32 fields (max_tx_size, max_block_body_size, max_block_header_size,
pool_retirement_epoch_bound, desired_number_of_pools, max_value_size,
collateral_percentage, max_collateral_inputs, committee_term_limit,
governance_action_validity_period, drep_inactivity_period) to call put_u32
instead; also fold min_committee_size into this helper path for consistency and
leave unwrap_u64 and put_u64_str for true u64 fields like coins_per_utxo_byte,
min_fee_coefficient, stake_key_deposit, pool_deposit, min_pool_cost,
governance_action_deposit, drep_deposit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68f3deb4-1401-44f0-b76c-303544d55df8

📥 Commits

Reviewing files that changed from the base of the PR and between 8975462 and cb0ff36.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • balius-proto/Cargo.toml
  • balius-proto/src/lib.rs
  • balius-proto/tests/wire_compat.rs
  • balius-runtime/Cargo.toml
  • balius-runtime/src/ledgers/u5c/convert.rs
  • balius-runtime/src/ledgers/u5c/mod.rs
  • balius-runtime/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • balius-proto/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • balius-proto/src/lib.rs
  • balius-runtime/src/lib.rs

…::v0

The crate's job has been "the Balius-owned wire schema", but the name
"balius-proto" advertised protobuf as the only thing it carries.
`balius-core` is a roomier name for what is really the foundational
types crate for the runtime <-> WASM interface — protobuf today, more
things possibly later.

The schema module also gets nested: `balius_proto::cardano` becomes
`balius_core::proto::v0::cardano`. The `v0` segment makes the versioning
explicit: this schema is wire-compatible with utxorpc-spec 0.17 by
construction (the entire point of this PR), so it is frozen for
pre-BigInt worker compat. A future breaking change to the schema goes
under `proto::v1`, leaving `v0` consumers untouched.

Mechanical changes:
 - Crate dir balius-proto/ -> balius-core/
 - Package name -> balius-core; description updated
 - src/cardano.rs -> src/proto/v0/cardano.rs (plus new mod.rs files)
 - Workspace member + all 3 dependent Cargo.tomls updated
   (balius-runtime, balius-sdk, examples/asteria-tracker)
 - Import paths: balius_proto::cardano -> balius_core::proto::v0::cardano
   across balius-sdk, balius-runtime, examples/asteria-tracker
 - Doc/comment references updated

No behavior change. cargo check --workspace --tests clean; 2/2 wire-compat
tests in balius-core; 9/9 lib tests in balius-runtime (incl. all 8 u5c
convert tests); e2e::faucet_claim still passes; only the pre-existing
u5c-chainsync::wallet_balance stale-wasm failure remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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