feat: expose append-only ledger health

This commit is contained in:
Navid
2026-08-29 09:57:58 +03:30
parent 32e7b6b749
commit 83720ba9d1
4 changed files with 28 additions and 19 deletions
+24 -16
View File
@@ -1,19 +1,20 @@
# General Ledger service design
Status: accepted for the first implementation slice on 2026-08-14.
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 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` 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 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.
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
@@ -114,13 +115,20 @@ 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.
- `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
@@ -155,7 +163,7 @@ are adapter concerns.
## Initial non-goals
- Replacing Stellar automatically on health-check failure.
- 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.
+2 -1
View File
@@ -21,9 +21,10 @@ func NewService(database Database) *Service {
}
func (s *Service) Check(ctx context.Context) Status {
status := Status{Serving: true}
status := Status{}
if s.database != nil {
status.DatabaseReady = s.database.Ping(ctx) == nil
}
status.Serving = status.DatabaseReady
return status
}
+1 -1
View File
@@ -22,7 +22,7 @@ func TestCheckReportsDatabaseReadiness(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
got := NewService(tc.db).Check(context.Background())
if !got.Serving || got.DatabaseReady != tc.ready {
if got.Serving != tc.ready || got.DatabaseReady != tc.ready {
t.Fatalf("unexpected status: %+v", got)
}
})
+1 -1
View File
@@ -13,7 +13,7 @@ func TestHealth(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !response.Serving || response.DatabaseReady {
if response.Serving || response.DatabaseReady {
t.Fatalf("unexpected response: %+v", response)
}
}