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

171 lines
8.5 KiB
Markdown

# 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.