diff --git a/DESIGN.md b/DESIGN.md index 941c331..69e399c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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. diff --git a/application/health/service.go b/application/health/service.go index 34f383d..764a920 100644 --- a/application/health/service.go +++ b/application/health/service.go @@ -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 } diff --git a/application/health/service_test.go b/application/health/service_test.go index 26134c4..b8d2d9e 100644 --- a/application/health/service_test.go +++ b/application/health/service_test.go @@ -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) } }) diff --git a/interface/grpc/health_test.go b/interface/grpc/health_test.go index 2707180..e955442 100644 --- a/interface/grpc/health_test.go +++ b/interface/grpc/health_test.go @@ -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) } }