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 # 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 ## Purpose and ownership
`GL` is Darano's durable financial journal. It records every committed wallet `GL` is Darano's primary financial source of truth. It records every wallet
value movement independently of Stellar so balances and transaction history can value movement as an append-only journal. Kuknos, through its Stellar-compatible
be reconstructed during a blockchain or provider outage. interface, is the secondary settlement and verification source of truth.
GL owns its PostgreSQL database and exposes an internal gRPC API. Wallet does 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 not write GL tables directly, and GL does not write wallet tables. In normal
operation Wallet remains the transaction orchestrator and Stellar remains the operation a transaction requires both GL and Kuknos. If GL is unhealthy, all
external settlement network. Promoting GL from a mirror to an operational value-changing transaction admission and processing halt until it recovers. If
fallback is an explicit, audited mode change; an outage must never make a failed Kuknos is unavailable, an authorized operator may explicitly enable GL-only
blockchain operation appear successful automatically. operation through an AdminPanel toggle or configuration. Kuknos must never be
disabled automatically.
## Non-negotiable invariants ## Non-negotiable invariants
@@ -114,13 +115,20 @@ does not hold a wallet database transaction open.
## Operating modes and failure semantics ## Operating modes and failure semantics
- `MIRROR`: normal mode. Wallet follows its existing settlement policy and GL - `NORMAL`: GL and Kuknos must both be healthy. A transaction is successful
asynchronously records every committed effect. only after its required GL journal and Kuknos settlement evidence exist.
- `DEGRADED_LEDGER`: explicitly enabled by an authorized operator. Eligible - `KUKNOS_DISABLED`: explicitly enabled and disabled by an authorized operator
internal operations may settle against GL while blockchain-bound operations through AdminPanel or configuration. GL remains mandatory and authoritative;
remain pending. The initial implementation does not activate this mode. eligible transactions may proceed without Kuknos. Every mode transition must
- `RECONCILE`: outbound posting is paused or restricted while tooling compares be immutable, attributable, time-bounded where configured, and emitted via
Wallet, GL, and blockchain state and appends approved reversals/corrections. 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 GL rejects unbalanced journals, invalid precision, unknown account/asset
combinations, duplicate line numbers, missing source identity, conflicting combinations, duplicate line numbers, missing source identity, conflicting
@@ -155,7 +163,7 @@ are adapter concerns.
## Initial non-goals ## Initial non-goals
- Replacing Stellar automatically on health-check failure. - Disabling Kuknos automatically on health-check failure.
- Editing or deleting posted journals. - Editing or deleting posted journals.
- Storing binary floats or using Wallet's mutable transaction table as GL. - Storing binary floats or using Wallet's mutable transaction table as GL.
- Sharing a database schema between Wallet and 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 { func (s *Service) Check(ctx context.Context) Status {
status := Status{Serving: true} status := Status{}
if s.database != nil { if s.database != nil {
status.DatabaseReady = s.database.Ping(ctx) == nil status.DatabaseReady = s.database.Ping(ctx) == nil
} }
status.Serving = status.DatabaseReady
return status return status
} }
+1 -1
View File
@@ -22,7 +22,7 @@ func TestCheckReportsDatabaseReadiness(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got := NewService(tc.db).Check(context.Background()) 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) t.Fatalf("unexpected status: %+v", got)
} }
}) })
+1 -1
View File
@@ -13,7 +13,7 @@ func TestHealth(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !response.Serving || response.DatabaseReady { if response.Serving || response.DatabaseReady {
t.Fatalf("unexpected response: %+v", response) t.Fatalf("unexpected response: %+v", response)
} }
} }