0001-modified-utxo-signed-postings.md 5.6 KB

Modified UTXO: value as signed postings, not mutable balances

  • Status: accepted
  • Authors: Cesar Rodas
  • Date: 2026-06-29
  • Targeted modules: kuatia-types, kuatia-core, kuatia-storage
  • Associated tickets/PRs: N/A

Context and Problem Statement

A ledger must record value movements so that the result is auditable, supports multiple assets, behaves well under concurrency, and can represent overdraft or credit lines. How should value be represented at rest? The naïve answer, a mutable balance field per (account, asset), throws away history and turns every account into a write-contended hot row. We want double-entry safety (Σ debits = Σ credits) without the brittleness of mutable running totals.

Decision Drivers

  • Auditability: the full financial state should be reconstructible by replaying recorded events, with no "trust me" mutable totals.
  • Multi-asset: one model for many assets, not a balance column per asset.
  • Concurrency: avoid a single hot balance row that serializes all activity on an account.
  • Conservation as a structural property: make "nothing is created or destroyed" a checkable invariant, not a convention.
  • Overdraft / credit: represent a negative position naturally, bounded by an account policy.
  • Embeddability: pure, deterministic core logic with no database arithmetic.

Considered Options

Option 1: Mutable balance fields per (account, asset)

A row holds the current balance; each transfer reads, mutates, and writes it.

Pros:

  • Good, because balance reads are O(1): just read the column.
  • Good, because it is the most familiar model.

Cons:

  • Bad, because history is lost: you cannot audit how a balance was reached without a separate, independently-trusted journal.
  • Bad, because the balance row is a concurrency hot spot; every transfer on an account contends on it.
  • Bad, because conservation is a convention enforced by application code, not a property of the data.

Option 2: Classic (Bitcoin-style) UTXO, non-negative outputs

Value is held as immutable outputs that are consumed and created; outputs cannot be negative.

Pros:

  • Good, because it is auditable (consume/create history) and avoids a hot row.
  • Good, because conservation is naturally expressible (sum in == sum out).

Cons:

  • Bad, because non-negative outputs cannot represent overdraft or credit lines.
  • Bad, because the Bitcoin model carries scripting/locking machinery irrelevant to an accounting ledger.

Option 3: Signed postings ("modified UTXO")

Value is held as signed postings: an immutable, signed amount of one asset owned by one account, with a lifecycle (Active → PendingInactive → Inactive). A transfer consumes postings and creates postings; a posting may be negative (an "offset position") to represent an overdraft.

Pros:

  • Good, because it keeps full history and is auditable by replaying the transfer log; balances are projections over Active postings and are never stored.
  • Good, because conservation is enforced directly: sum(consumed) == sum(created) per asset on every committed transfer (validate_and_plan, ConservationViolation).
  • Good, because it is multi-asset by construction (a posting names its asset).
  • Good, because there is no hot balance row: different postings of the same account can be touched independently, enabling UTXO-style concurrency and future sharding.
  • Good, because overdraft is just a negative posting, bounded by account policy (NoOverdraft, CappedOverdraft { floor }, UncappedOverdraft, and so on).

Cons:

  • Bad, because a balance is computed (sum over Active postings), not read: a read cost the store must support efficiently.
  • Bad, because the word posting (a noun: a value fragment) collides with the accounting verb "to post", a documented source of confusion.
  • Bad, because spending requires posting selection and change-making (greedy largest-first), machinery a mutable-balance model would not need.

Decision Outcome

Chosen option: Option 3, signed postings ("modified UTXO"), because it is the only option that delivers auditability, a hot-row-free concurrency model, and natural overdraft, while making per-asset conservation a structural, checkable invariant. The "modification" over classic UTXO is precisely that postings may be negative (offset positions) and there is no scripting layer.

Positive Consequences

  • The transfer log (TransferStore of EnvelopeRecords) is the append-only source of truth; balances are derived, so they cannot silently diverge.
  • Double-entry safety is enforced in the pure core and testable with golden vectors, independent of any storage backend.
  • The posting lifecycle (Active/PendingInactive/Inactive) gives the saga a place to reserve inputs without mutating balances. See ADR-0002 and ADR-0003.

Negative Consequences

  • Balance queries sum over Active postings; the store indexes (owner, asset, status) to keep this cheap, and all such arithmetic is done in Rust with checked operations (never in SQL).
  • Posting selection / change-making is required on the spend path.
  • Terminology must be taught: see accounting-mapping.md for the classical-accounting to Kuatia mapping and the noun/verb caveat.

Links