171 lines
8.5 KiB
Markdown
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.
|