docs(gl): define ledger architecture and invariants
This commit is contained in:
@@ -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:<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
|
||||||
|
|
||||||
|
- `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.
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
# GL
|
# 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).
|
||||||
|
|||||||
Reference in New Issue
Block a user