Files
GL/DESIGN.md
T
2026-08-29 09:57:58 +03:30

8.5 KiB

General Ledger service design

Status: append-only model accepted on 2026-08-14; authority and availability policy revised on 2026-08-28.

Purpose and ownership

GL is Darano's primary financial source of truth. It records every wallet value movement as an append-only journal. Kuknos, through its Stellar-compatible interface, is the secondary settlement and verification source of truth.

GL owns its PostgreSQL database and exposes an internal gRPC API. Wallet does not write GL tables directly, and GL does not write wallet tables. In normal operation a transaction requires both GL and Kuknos. If GL is unhealthy, all value-changing transaction admission and processing halt until it recovers. If Kuknos is unavailable, an authorized operator may explicitly enable GL-only operation through an AdminPanel toggle or configuration. Kuknos must never be disabled automatically.

Non-negotiable invariants

  1. Every monetary journal is double entry. Signed entries sum to zero for each asset in a journal.
  2. Journals and entries are append-only. Corrections append a full reversal and a replacement; update and delete operations are not exposed.
  3. Amounts cross the API as canonical decimal strings and are stored as numeric(38,18). Floating-point values are rejected at the GL boundary.
  4. A source idempotency key identifies one immutable payload. Replaying the same key and payload succeeds with the original result; reusing the key for different content fails.
  5. Account, asset, source transaction, correlation, actor, occurrence time, and recording time are retained on every applicable record.
  6. A journal commits atomically with all its entries or not at all.
  7. A financial effect is journaled only when Wallet has committed that effect. Pending and failed lifecycle events may be retained as audit events but do not create monetary entries.
  8. Derived balances are rebuildable solely by ordering and summing immutable entries. Cached balances are disposable projections, never source data.

Data model

ledger_accounts

A stable account is scoped to an asset and optional owner. Initial account classes are USER_AVAILABLE, USER_FROZEN, EXTERNAL_BLOCKCHAIN, TREASURY, MARKET_CLEARING, IPG_CLEARING, and COMMISSION_REVENUE. The natural identity (class, owner_type, owner_id, asset_id) is unique.

journals

Each journal contains a generated UUID, source service, unique idempotency key, source transaction ID and tracking code, effect kind and version, optional reversal target, occurred/recorded timestamps, correlation and actor IDs, blockchain network/hash/sequence references, and JSON metadata. A canonical payload hash detects conflicting reuse of an idempotency key.

entries

Each entry contains its journal ID, line number, ledger account, asset ID, signed decimal amount, and optional description. (journal_id, line_number) is unique. Positive means credit and negative means debit. Zero entries are invalid. Database constraints and the application transaction jointly enforce precision, immutability, account/asset agreement, and per-asset balance.

transaction_events

Lifecycle observations such as CREATED, PENDING_TRX, PENDING_ADMIN, FAILED, and SUCCESSFUL are append-only events keyed idempotently by source transaction and event version. They preserve error and blockchain metadata but are separate from monetary journals.

Wallet-to-ledger mapping

Every row below balances independently per asset. Where one business operation moves two assets, the entries belong to one correlated journal containing two balanced asset groups.

Wallet effect Debit Credit
Internal transfer sender USER_AVAILABLE recipient USER_AVAILABLE
External deposit EXTERNAL_BLOCKCHAIN recipient USER_AVAILABLE
External withdrawal sender USER_AVAILABLE EXTERNAL_BLOCKCHAIN
Freeze/lock user USER_AVAILABLE user USER_FROZEN
Release/unlock user USER_FROZEN user USER_AVAILABLE
IRT/IPG deposit IPG_CLEARING user USER_AVAILABLE
IRT withdrawal user USER_AVAILABLE IPG_CLEARING
Commission payer or originating clearing account COMMISSION_REVENUE
Buy/ICO buyer IRT; treasury asset inventory TREASURY IRT; buyer asset
Sell/redeem seller asset; TREASURY IRT treasury asset inventory; seller IRT
Market trade buyer quote and seller base seller quote and buyer base

Trustline creation is non-monetary and is recorded only as a transaction event. The adapter must derive accounts from stable IDs, not display names or public keys. Before integration, each existing Wallet call site must confirm its exact counterparty and fee legs; missing information is a validation failure, not an implicit suspense posting.

Delivery and ordering

Wallet owns a durable ledger_outbox table. The wallet state change, wallet transaction update, and outbox insert occur in the same PostgreSQL transaction. A dispatcher delivers events to GL with at-least-once semantics. It retries with backoff and never drops a record; poison records are quarantined and alerted but remain replayable.

The idempotency key format is wallet:<transaction-id>:<effect-kind>:v<event-version>. A transaction hash is metadata, not an identity, because it can be absent or replaced. GL serializes posts that affect the same accounts, records both source and receipt ordering, and returns the existing journal for identical duplicates. Together, the transactional outbox and GL idempotency provide effectively-once posting.

Wallet must not synchronously dual-write its database and GL. GL/network unavailability therefore cannot lose an already committed wallet effect and does not hold a wallet database transaction open.

Operating modes and failure semantics

  • NORMAL: GL and Kuknos must both be healthy. A transaction is successful only after its required GL journal and Kuknos settlement evidence exist.
  • KUKNOS_DISABLED: explicitly enabled and disabled by an authorized operator through AdminPanel or configuration. GL remains mandatory and authoritative; eligible transactions may proceed without Kuknos. Every mode transition must be immutable, attributable, time-bounded where configured, and emitted via OpenTelemetry. This mode is not implemented yet.
  • RECONCILE: transaction processing is paused or restricted while tooling compares GL and Kuknos and appends approved reversals/corrections. Existing ledger records are never edited or deleted.

GL failure is always fail-closed. Public readiness reports a critical state and the incident and recovery are emitted through OpenTelemetry. Kuknos failure is also fail-closed unless KUKNOS_DISABLED has been explicitly authorized.

GL rejects unbalanced journals, invalid precision, unknown account/asset combinations, duplicate line numbers, missing source identity, conflicting idempotency payloads, and invalid reversals. Identical retries are successful. If Wallet cannot write its outbox in the local transaction, that wallet transaction rolls back. If dispatch fails, Wallet records retry state without changing the committed financial result.

Internal service boundary

The first ledger.v1.GeneralLedgerService contract provides:

  • AppendJournal for one atomic, idempotent balanced journal;
  • AppendTransactionEvent for non-monetary lifecycle history;
  • GetJournal and ListEntries for audit and replay inspection;
  • GetBalance for a rebuildable account/asset projection;
  • Health for readiness and database health.

Wallet depends on its own Ledger port. Its gRPC implementation translates wallet domain values to this contract; Wallet never imports GL persistence types. Deadlines, retry classification, authentication, and correlation metadata are adapter concerns.

Implementation sequence

  1. Add backward-compatible ledger/v1 protobuf messages and service methods.
  2. Scaffold GL with domain, application, infrastructure, and interface layers.
  3. Add explicit SQL migrations and a transactional PostgreSQL repository.
  4. Implement invariant validation, idempotency, queries, and reversals.
  5. Add Wallet's ledger port, gRPC adapter, transactional outbox, and dispatcher.
  6. Map each Wallet transaction path and add outage/replay/reconciliation tests.

Initial non-goals

  • Disabling Kuknos automatically on health-check failure.
  • Editing or deleting posted journals.
  • Storing binary floats or using Wallet's mutable transaction table as GL.
  • Sharing a database schema between Wallet and GL.
  • Migrating UI, documentation-site, or DevOps repositories.