From 1863de1f3be0118ea98d41e81f1f9f785b0da2c4eb915fb5867a05264c718603 Mon Sep 17 00:00:00 2001 From: nfel Date: Fri, 14 Aug 2026 18:45:02 +0330 Subject: [PATCH] docs(gl): define ledger architecture and invariants --- DESIGN.md | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 ++ 2 files changed, 167 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..941c331 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,162 @@ +# General Ledger service design + +Status: accepted for the first implementation slice on 2026-08-14. + +## Purpose and ownership + +`GL` is Darano's durable financial journal. It records every committed wallet +value movement independently of Stellar so balances and transaction history can +be reconstructed during a blockchain or provider outage. + +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 Wallet remains the transaction orchestrator and Stellar remains the +external settlement network. Promoting GL from a mirror to an operational +fallback is an explicit, audited mode change; an outage must never make a failed +blockchain operation appear successful 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:::v`. 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 + +- `MIRROR`: normal mode. Wallet follows its existing settlement policy and GL + asynchronously records every committed effect. +- `DEGRADED_LEDGER`: explicitly enabled by an authorized operator. Eligible + internal operations may settle against GL while blockchain-bound operations + remain pending. The initial implementation does not activate this mode. +- `RECONCILE`: outbound posting is paused or restricted while tooling compares + Wallet, GL, and blockchain state and appends approved reversals/corrections. + +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 + +- Replacing Stellar 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. diff --git a/README.md b/README.md index a6a9bdf..c6eb249 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # GL +Darano's independent, append-only general-ledger service. It preserves every +committed wallet value movement so financial state can be reconstructed when a +blockchain or provider is unavailable. + +The implementation contract and invariants are defined in [DESIGN.md](DESIGN.md).