Files
GL/deployment/AUTHORITY_API.md
T
nfel 88e227f80c
CI/CD Stage / stage (push) Failing after 1m12s
feat(gl): add standalone ledger authority mode
2026-10-06 22:40:07 +03:30

6.6 KiB

GL authority API

The ledger.v1.GeneralLedgerService gRPC service exposes four additional RPCs. The contract is in ../proto/ledger/v1/{msg,srv}.proto from the GL directory; the generated Go client is in gen/ledger/v1.

Operator toggle

In the existing AdminPanel, open GL journals → GL operating mode. The checkbox, required reason, and recovery confirmation call GL directly. The operator's ID comes from the authenticated session. Permission ledgerops.set_operating_mode is required; superusers already have it. Operators also need ledgerops.view_generalledgerjournal for journal navigation. Register the permission using AdminPanel's usual Django migrate command and grant it only to designated operators. Configure the existing GL_ADMIN_GRPC_ADDRESS and GL_ADMIN_GRPC_TOKEN settings, with the token matching GL's DARANO_GL_ADMIN_TOKEN. These are server-side settings.

The AdminPanel control changes GL's authority state. Existing Wallet flows still need to call the internal posting API; changing Wallet's old kuknos-enabled configuration alone continues to select read-only behavior.

Future Wallet routing contract

Wallet remains the entry point in both modes. The public Wallet RPCs, request and response schemas, authentication, and client UI stay unchanged; clients must not call GL directly or choose the execution mode.

GL mode Transaction execution User balance response
NORMAL (default) Existing Wallet admission → GL settlement queue → Stellar/Kuknos Existing Wallet behavior
GL_ONLY Wallet admission → GL PostInternalJournal, with no blockchain call Wallet returns GL GetUserBalances values in its existing response format

This routing is reserved for future activation. The GL backend and AdminPanel control exist, but the Wallet routing above is not implemented yet. Keep NORMAL selected for the current Wallet deployment. Enabling the control now pauses settlement and causes existing Wallet financial writes to fail closed; it does not automatically turn those endpoints into internal execution.

The future implementation must retain Wallet's authorization and business validation, persist the execution choice and exact command for recovery, and use the same transaction identity for retries. A GL commit establishes financial success; subsequent Wallet history updates are recoverable projections and must not produce a second financial journal through the normal outbox. Reconciliation must preserve the original execution choice across mode changes rather than reinterpret an older blockchain transaction as an internal transfer. Cutover requires draining normal-mode work and checking opening balances. Returning to normal requires reconciliation before external settlement resumes.

Internal transfers, freezes, releases, trades, and their fees can execute inside GL. An actual external blockchain payment cannot be reported as externally settled while Stellar/Kuknos is unavailable.

GetOperatingMode(base.v1.Empty) returns mode, revision, actor_id, reason, and changed_at.

SetOperatingMode requires exactly one x-admin-token metadata value matching DARANO_GL_ADMIN_TOKEN. To enable internal authority, send:

{
  "mode": "GL_ONLY",
  "expectedRevision": "0",
  "actorId": "operator-17",
  "reason": "Kuknos outage; opening balances and outbox checked"
}

Use the revision from the latest read, rather than always sending zero. Mode persists across service restarts. A concurrent toggle returns ABORTED; read the state before deciding whether another change is needed. A toggle with in-flight settlements or negative user balances returns FAILED_PRECONDITION. Returning to NORMAL requires reconciliationConfirmed: true after operator reconciliation.

Internal execution

PostInternalJournal accepts the existing AppendJournalRequest and requires the administrative token, an actor ID, and GL_ONLY mode for new postings. For a transfer, debit the sender's USER_AVAILABLE account and credit the recipient's account by the same exact decimal amount. For a freeze, debit available and credit frozen; for a release, reverse those legs. Multi-asset trade and fee legs must balance independently per asset.

The API accepts exact balanced entries so it can represent all internal business effects without depending on Stellar XDR, account sequences, or a Horizon endpoint. It does not determine business fees, asset eligibility, or whether the caller's user is allowed to move those funds; trusted application admission must supply and authorize those legs. It forbids blockchain references and EXTERNAL_BLOCKCHAIN accounts. The ordinary journal API remains the normal-mode ingestion interface and rejects new effects in GL_ONLY, while allowing identical retries of previously ingested journals.

Success means the journal and every entry committed atomically. Insufficient available or frozen funds returns FAILED_PRECONDITION. Retry uncertain outcomes with the exact same idempotency key and payload. Identical retries return already_existed=true; conflicting payloads return ALREADY_EXISTS. A concurrency abort returns ABORTED; retry that same request rather than allocating a new transaction identity. Database errors never mean success.

The server reserves journal metadata key gl_execution and stores GL_ONLY as immutable execution provenance. Callers do not set it.

User balances

GetUserBalances reads GL entries without any blockchain call:

{"ownerType": "user", "ownerId": "17", "assetId": "7"}

Example response:

{
  "ownerType": "user",
  "ownerId": "17",
  "balances": [
    {"assetId": "7", "available": "34", "frozen": "6", "total": "40"}
  ]
}

Omit assetId to return all journaled assets for that owner. An unknown owner has an empty list; a requested asset with no entries returns zero amounts. Amounts are decimal strings. Available and frozen are read in one database snapshot. The endpoint is available in either operating mode.

The API is an internal service: expose it through an authenticated application gateway that binds the requested owner to the logged-in user. Do not expose arbitrary owner lookups or the operator token directly to a browser.

Verification

The opt-in PostgreSQL test creates and removes its own isolated schema:

GL_TEST_DATABASE_URL='<disposable PostgreSQL connection URL>' \
  go test -race ./infrastructure/postgres -run TestAuthorityWithPostgres -v

It exercises mode authentication, revision conflicts, immutable audit records, settlement suspension, concurrent overspending prevention, idempotent replay, freezes/releases, internal RPC posting, and user balance reconstruction.