Repository navigation
Description
Summary
pkgs/id/src/currency_poc/tb_ledger.rs — the LedgerBackend over the real TigerBeetle client — maps TigerBeetle result statuses to the PoC's typed errors through two functions with a panic catch-all:
map_account_status(tb_ledger.rs:186–199):other => panic!("tigerbeetle create_accounts returned unmodelled status {other:?}")map_transfer_status(tb_ledger.rs:202–242):other => panic!("tigerbeetle create_transfers returned unmodelled status {other:?}")
The module docs (tb_ledger.rs:25–29) call this deliberate — "The scenarios exercise only the mapped subset" — but several unmodelled statuses are not exotic: they are the ordinary per-event validation results for input shapes the PoC types themselves accept, and for state a real cluster can be in. When one arrives, the panic unwinds out of create_accounts/create_transfers (tb_ledger.rs:277/322) and crashes the calling executor/gateway process instead of returning the per-leg Result the LedgerBackend trait promises.
Evidence (verified this run, laptop primary @ c7a0982, HEAD == origin/main)
- The pinned client is
tigerbeetle = "=0.17.9"(pkgs/id/Cargo.toml:116). In that crate,CreateTransferStatushas ~70 variants andCreateAccountStatus~27; the mappers model roughly a third of the transfer set and 9 account statuses. - Unmodelled transfer statuses reachable from ordinary inputs/state:
IdMustNotBeZero(5),IdMustNotBeIntMax(6),DebitAccountIdMustNotBeZero(8),CreditAccountIdMustNotBeZero(10),DebitAccountIdMustNotBeIntMax(9),CreditAccountIdMustNotBeIntMax(11),FlagsAreMutuallyExclusive(7),DebitAccountAlreadyClosed(65),CreditAccountAlreadyClosed(66). (Also unmodelled, if less reachable today:CodeMustNotBeZero(20) — codes pass throughwire_code's +1 shift —OverflowsTimeout(53), and the pending-transfer and imported-event families.) - Unmodelled account statuses reachable the same way:
IdMustNotBeZero(6),IdMustNotBeIntMax(7),CodeMustNotBeZero(14),FlagsAreMutuallyExclusive(8),DebitsPendingMustBeZero/DebitsPostedMustBeZero/CreditsPendingMustBeZero/CreditsPostedMustBeZero(9–12), andLinkedEventFailed(1) /LinkedEventChainOpen(2) for linked account batches — both of which ARE modelled on the transfer side. - Backend divergence (probe, below): the in-memory reference backend accepts these shapes — an account with
id = 0is createdOk, a transfer withid = 0postsOk, and a transfer debiting account 0 postsOkonce account 0 exists. So a scenario exercised only againstInMemoryLedger(the default insuper::tests) passes, while the same batch against a real cluster returns e.g.IdMustNotBeZeroand panics the process. - Probe (laptop,
~/git/fleet/probe-tb-unmodelled, realidcrate + pinnedtigerbeetle0.17.9 crate; the two mappers copied verbatim from tb_ledger.rs:186–243 since they are private):P1 create_accounts incl id=0: [Ok(()), Ok(()), Ok(())]— in-memory backend creates account 0.P1 create_transfers transfer id=0: [Ok(())]andP1 create_transfers debit_account_id=0 (account 0 exists): [Ok(())]— the same shapes post fine in memory.P2 transfer IdMustNotBeZero / DebitAccountIdMustNotBeZero / CreditAccountIdMustNotBeZero / IdMustNotBeIntMax / CodeMustNotBeZero / DebitAccountAlreadyClosed / OverflowsTimeout: PANIC (unmodelled)— all seven.P2 account IdMustNotBeZero / CodeMustNotBeZero / LinkedEventFailed / IdMustNotBeIntMax: PANIC (unmodelled)— all four.- Control:
P2 control LedgerMustNotBeZero transfer: Err(LedgerMismatch)— modelled statuses map as designed.
- No existing filing:
ghsearches formap_transfer_status,tb_ledger,IdMustNotBeZero, and "tigerbeetle panic unmodelled status" (all states) return no covering issue. Adjacent but distinct: id(conversion): build_conversion_batch accepts ledger 0 — builds a batch that can never post, with no typed pre-ledger rejection #6102 is aboutbuild_conversion_batchaccepting ledger 0 at the pure layer; this filing is the TB binding's status mapping, which panics for any caller of the trait, conversion or not.
Why it matters
The LedgerBackend contract is per-leg Results: the executor and the recovery machinery treat a rejected leg as data to classify and persist, not as a process-fatal event. A single malformed or unlucky leg — a zero/int-max ID from a bad ID generator, a transfer against an account that has since been closed, a timeout overflow on a pending transfer — currently takes down the whole process on the real backend while the in-memory backend (what CI exercises) sails through. That is exactly the class of divergence a two-backend design exists to prevent, and it hides until the TB harness or a real deployment hits it.
Acceptance criteria
- Every
CreateAccountStatus/CreateTransferStatusvariant of the pinned tigerbeetle crate that is reachable from inputs the PoC types accept (zero/int-max account and transfer IDs, closed accounts, mutually exclusive flags, non-zero initial balances, linked-batch failures) maps to a typedAccountError/TransferError— new variants added where no existing one fits — instead of reaching the panic arm. - A unit test exercises both mappers over every variant of the pinned crate's two status enums and asserts none of them panics (an exhaustiveness net that fails loudly on the next crate bump instead of in production).
- Any status intentionally left panic-worthy (e.g. imported-event statuses the binding can never emit) is enumerated explicitly in the match with a comment saying why it is unreachable, rather than falling into a bare catch-all.
- The in-memory backend's behaviour for the zero-ID shapes is reconciled or explicitly documented as divergent, so scenario tests mean the same thing on both backends.
cargo test -p id --lib currency_pocpasses (with thecurrency-poc-tbfeature where the harness requires it).
Scope notes
- Filed-only from the creator lane; not self-solved — the new error-variant shapes are the currency executor author's call, and the fix touches the recovery classification surface.
Activity
developing-today commentedon Nov 5, 2026
CODE-1760
dezren39 commentedon Nov 5, 2026
🤖 Agent note · state: filed
dezren39 commentedon Nov 5, 2026
🤖 Agent note · picked up
144 remaining items
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencodedezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off