Skip to content

id(conversion): build_conversion_batch accepts ledger 0 — builds a batch that can never post, with no typed pre-ledger rejection #6102

Open

Description

@dezren39

Summary

pkgs/id/src/conversion.rs — the production pure conversion layer (Phase E of #2063, landed 2026-11-04 under #5536) — builds a linked conversion batch for source_ledger = 0 or dest_ledger = 0 without any rejection. A zero ledger can never post: every layer below the builder refuses it, so the batch the Phase F executor will persist and submit is unpostable by construction, and the failure surfaces only at ledger submission time instead of as a typed pre-ledger rejection.

Evidence (verified this run, laptop primary @5f161d2e3c)

  • build_conversion_batch (pkgs/id/src/conversion.rs, the validation sequence in build_conversion_batch) checks, in order: zero amount, min/max bounds, rate/scale/fee terms (via convert_amount/fee_amount), same-account for the source/fee/destination legs, fee-account presence, and transfer-ID count/duplicates. There is no check on source_ledger or dest_ledger anywhere in the module — grep ledger pkgs/id/src/conversion.rs shows the fields only copied into the built Transfers.
  • The ledger layer the batch feeds refuses ledger 0 twice over:
    • pkgs/id/src/currency_poc/ledger.rs — AccountError::LedgerMustNotBeZero ("ledger must be non-zero"): no account can even be created on ledger 0.
    • validate_and_stage in the same file: if transfer.ledger == 0 || … → TransferError::LedgerMismatch for every leg of a zero-ledger batch.
    • Real TigerBeetle likewise requires a non-zero ledger (see pkgs/id/src/currency_poc/tb_ledger.rs module docs on non-zero wire fields).
  • Probe (laptop, ~/git/fleet/probe-conversion-ledger, real id crate @5f161d2e3c): build_conversion_batch with source_ledger=0 and with dest_ledger=0 both return Ok with a full 2-leg chain; submitting the ledger-0 legs to InMemoryLedger fails every leg, and create_accounts on ledger 0 returns LedgerMustNotBeZero. The sane-ledger control (100/200) builds and posts. Probe output is quoted in the filing agent note.

Why it matters

The module's own contract is that rejections happen before any ledger write, with typed reasons: "ConvertError — Why a conversion quote or batch cannot be built. Every variant is a rejection before any ledger write, per the failure table in #2063 §2.4." The builder already mirrors the ledger's other hard rules as typed pre-ledger errors — SameAccount mirrors AccountsMustBeDifferent, ZeroAmount mirrors AmountMustNotBeZero, duplicate-ID mirrors the staged-transfer ID check. Ledger-zero is the one ledger hard rule with no mirror: a caller (or a bad gateway resolution in Phase B/F) that supplies ledger 0 gets a persisted intent and a submitted batch that can only fail at the ledger, where the executor's recovery machinery (#4669 territory) then has to classify a failure that was knowable at build time.

Related but distinct: #4071 covers RateAuthorisation::validate accepting zero/same ledgers in the authorisation body. This filing is the new pure batch layer, which takes raw ledger fields (ConversionBatchRequest.source_ledger/dest_ledger) and does not consume a validated authorisation — fixing #4071 does not add a check here.

Acceptance criteria

  • build_conversion_batch rejects source_ledger == 0 and dest_ledger == 0 with a new typed ConvertError variant (e.g. ZeroLedger { leg: "source" | "destination" } or equivalent), before any leg is built.
  • The rejection order stays deterministic and is documented alongside the existing validation-order note in build_conversion_batch's docs.
  • Pinning tests in pkgs/id/src/conversion.rs cover: source ledger 0 rejected, destination ledger 0 rejected, and the existing sane-ledger requests (100/200) still build identical batches.
  • cargo test -p id --lib conversion passes.

Scope notes

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    on Nov 5, 2026
  2. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor
  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 5, 2026
  4. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-11-05 00:35 CST
    • Found via: filed this run
    • Task: Filed after verifying on the laptop primary @5f161d2e3c with probe ~/git/fleet/probe-conversion-ledger (real id crate): build_conversion_batch returns Ok for source_ledger=0 (legs stamped ledger 0/200) and for dest_ledger=0; InMemoryLedger::create_accounts on ledger 0 returns [Err(LedgerMustNotBeZero), Err(LedgerMustNotBeZero)] and the built legs submit as [Err(LedgerMismatch), Err(LinkedEventFailed)], while the 100/200 control builds and posts [Ok, Ok]. No ledger check exists anywhere in pkgs/id/src/conversion.rs. Distinct from id(currency): RateAuthorisation::validate accepts internally inconsistent bodies — min>max, empty/inverted window, fee_bps>10000, empty/same ledgers #4071 (RateAuthorisation body validation; this layer takes raw ledger fields). Enqueued as a repo-task/v1 triage packet on repo-health-code-pilot.
    • Expectation: effort/s — triage confirms the typed ConvertError variant shape, then a small fix adds the check plus pinning tests.
    • Links: body file hidden_files/issue-id-conversion-zero-ledger.md; probe ~/git/fleet/probe-conversion-ledger (laptop)
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  6. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  7. 115 remaining items

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  9. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 6, 2026
  11. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  12. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  14. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions