مستندات دارا
مرجع فنیاستلار
این صفحه فقط به انگلیسی موجود است.

Stellar Architecture (Stages 6, 8, 9, 10)

Scope: account topology, authority, trustlines, authorization, issuance/burn mapping, sequence management, the transaction lifecycle, RPC, indexing, supply reconciliation and the validator decision. Decisions referenced: Q-01–Q-04, D-S01, D-E01–D-E05 (../architecture/DECISIONS.md). Protocol facts: ../discovery/STELLAR_TECHNOLOGY_BASELINE.md.

Protocol assumptions marked [verified] were tested on Quickstart (P30): ../testing/evidence/STELLAR_ASSUMPTIONS_SPIKE_20261029.md.

1. Account topology

flowchart TB
  subgraph Authority["Issuer account (per asset family; master weight 0)"]
    ISS["ISSUER G…<br/>flags: AUTH_REQUIRED, AUTH_REVOCABLE, AUTH_CLAWBACK_ENABLED"]
  end
  H["Issuer signers H1…Hn<br/>(human keys, weight 10 each; launch: one)"] -. sign .-> ISS
  AUTH["Authorization signer<br/>(online key, weight 1)"] -. sign low only .-> ISS

  ISS -- "ISSUE = Payment from issuer" --> COLD["TREASURY_COLD<br/>(human multisig)"]
  COLD -- "refill (four-eyes, multisig)" --> HOT["TREASURY_HOT<br/>(online key)"]
  HOT -- "sweep above ceiling" --> COLD
  COLD -- "BURN = Payment to issuer" --> ISS
  HOT -- "BUY settlement" --> C1["Customer account A<br/>(signer: custody key)"]
  C1 -- "SELL settlement" --> HOT
  C1 -- "TRANSFER" --> C2["Customer account B"]

  SP["RESERVE_SPONSOR<br/>(pays customer reserves)"] -. sponsors .-> C1
  SP -. sponsors .-> C2
  FUND["FUNDER<br/>(tops up channels)"] --> CH["Channel accounts CH1…CHn<br/>(tx source: sequence + fee)"]
AccountPurposeHolds asset?Signers (MVP)Created by
ISSUERAsset identity; issuance, authorization, clawbackNever (issuer balances do not exist)H1…Hn (weight 10; launch: one signer), AUTH (weight 1); master 0Asset ceremony
TREASURY_COLDReceives issuance; source of burns; reserve inventoryYesHuman treasury signers (multisig); master 0Ceremony
TREASURY_HOTOperational float for BUY; receives SELLYesHOT online key + human signers for high ops; master 0Ceremony
RESERVE_SPONSORSponsors customer account + trustline + signer reservesNoSPONSOR online key; master 0Ceremony
FUNDERHolds operational XLM; tops up channelsNoFUNDER online keyCeremony
CHANNEL_nTransaction source: sequence numbers and feesNoCHANNEL online key (shared); master 0Bootstrap by FUNDER
GOV_CHANNELTransaction source for human-signed proposals onlyNoCHANNEL keyBootstrap
Customer accountCustodial holding account, one per customerYesCUSTODY online key (shared across customers, weight 1); master 0Onboarding (§4)

Rules:

  1. The issuer, Treasury and customer accounts are never transaction sources. They appear only as operation sources. Their sequence numbers stay frozen, so (a) a pending multisig proposal can never be invalidated by routine activity, and (b) concurrency is handled entirely in the channel pool (§6).
  2. One issuer per asset. The issuer's flags and signers then apply to exactly one asset, and one asset's incident cannot affect another. Cost: one more ceremony per asset.
  3. Hot/cold split: TREASURY_HOT holds at most a configured float (Q-08); everything else stays in TREASURY_COLD. This bounds the loss if the online hot key is compromised (brief §95). For reconciliation, "Treasury inventory" = HOT + COLD.
  4. Customer accounts hold no secret keys at rest. Each is created with a throwaway keypair whose only use is to sign the creation transaction; the account's master weight is set to 0 there, and the shared CUSTODY key is added as the only signer. A production HSM therefore holds one customer key, not one per customer (same reasoning as the siblings' D-018).

2. Authority (summary; full matrix in ../security/AUTHORITY_MATRIX.md)

Issuer thresholds follow from the number of human signers (ceremony.Quorum, ../security/AUTHORITY_MATRIX.md §1): launch, one signer: low = 1, medium = 10, high = 10; reference design, four signers: 1 / 20 / 30. Each human signer has weight 10, AUTH weight 1, so AUTH never reaches the medium threshold.

ActionStellar operationClassWho can reach it (launch: one signer · four signers)
Authorize / revoke / restore a customer trustlineSetTrustLineFlagsLow (1)AUTH alone (online), or any human
IssuePayment issuer → TREASURY_COLDMediumThe signer · any 2 of 4. AUTH alone: cannot (TestSingleHumanSigner)
ClawbackClawbackMediumThe signer · any 2
Home domain, flagsSetOptions (no signer change)MediumThe signer · any 2
Change signers or thresholdsSetOptions (signers/thresholds)HighThe signer · any 3 of 4 (one lost key still leaves 3). With one signer a lost key cannot be repaired

A compromised AUTH key can authorize a stranger's trustline (useless: only our Treasury and customers send the asset, and their keys are separate) or revoke customers' authorization (denial of service, reversible by AUTH/humans). It cannot create, move or destroy tokens.

3. Asset identity and configuration

  • Identity = asset_code + ISSUER + network passphrase. The backend stores all three and refuses to start if the configured issuer, Treasury and channel accounts or the passphrase disagree with the environment manifest and with getNetwork (brief §70).
  • Codes: GOLD (alphanum4) at launch (Q-13, decided 2026-10-31). Later assets (for example SILVER) follow the same model.
  • Precision: 7 decimals, fixed by Stellar. The platform stores amounts as integer stroops (int64 on-chain, NUMERIC(20,0) in PostgreSQL).
  • Flags are set by the ceremony before any trustline exists; the ceremony verifies TRUSTLINE_CLAWBACK_ENABLED on the first trustline (ASSET_CONFIGURATION.md).
  • Metadata (SEP-1). On every non-local network the issuer's home_domain points to our /.well-known/stellar.toml, published before activation. It has one [[CURRENCIES]] entry per asset: code, issuer, display_decimals (= the asset's display precision), name, desc, is_asset_anchored = true, anchor_asset_type = "commodity", anchor_asset (the unit definition, e.g. "1 g of 750 gold"), attestation_of_reserve (the latest published attestation), redemption_instructions, and a plain statement of the issuer's freeze and clawback powers. Wallets and explorers identify the asset by this file. Its fields must match the database and the chain: activation and the scanner check this. We do not set regulated = true: that flag announces a SEP-8 approval server, which a custodial platform does not need (see Q-09).

4. Customer onboarding (trustline as a product concept)

Triggered when Compliance approves KYC. One transaction, source CHANNEL_k [verified]:

#OperationOp sourceSigned by
1BeginSponsoringFutureReserves(new)RESERVE_SPONSORSPONSOR
2CreateAccount(new, 0 XLM)RESERVE_SPONSORSPONSOR
3ChangeTrust(GOLD, limit = max) per enabled assetnewthrowaway key
4SetOptions(signer CUSTODY weight 1, master 0, thresholds 1/1/1)newthrowaway key
5EndSponsoringFutureReservesnewthrowaway key (signatures are checked against the state before the transaction applies)
6SetTrustLineFlags(new, GOLD, set AUTHORIZED) per assetISSUERAUTH

Signature set: channel, SPONSOR, throwaway, AUTH, and nothing else. An unused signature (for example CUSTODY) makes the envelope invalid (txBAD_AUTH_EXTRA, found in the Stage 14 integration tests). The throwaway key is derived in the signer from the customer ID (onboard:<uuid>), so an expired onboarding is re-signed with the same key; the persisted envelope is enough for rebroadcast. The wallet shows only "Activating your GOLD wallet…", then "Ready". Before any payment to an account, the gateway checks (error names from brief §23): ACCOUNT_NOT_FOUND, TRUSTLINE_REQUIRED, TRUSTLINE_NOT_AUTHORIZED, TRUSTLINE_LIMIT_EXCEEDED, INSUFFICIENT_RESERVE.

Enabling a later asset for existing customers is the same transaction without ops 2 and 4 (sponsored ChangeTrust + authorize).

5. Authorization state machine

Business stateStellar trustline flagsCan sendCan receiveEntered by
PENDINGno trustline yet, or unauthorized——Account created
ACTIVEAUTHORIZEDyesyesKYC approval
FROZENAUTHORIZED_TO_MAINTAIN_LIABILITIES* or unauthorizednonoCompliance freeze (four-eyes: no; single Compliance officer, audited)
ACTIVE (restored)AUTHORIZEDyesyesUnfreeze (four-eyes)
CLOSEDtrustline removed after a zero balance——Account closure (future)

* We use fully unauthorized for FROZEN, since we have no offers; AUTHORIZED_TO_MAINTAIN_LIABILITIES only matters for DEX offers. The platform freeze also blocks quotes and orders off-chain, so the on-chain freeze is defense in depth.

SUSPENDED (an off-chain trading block with no chain effect) and REVOKED (KYC failure, terminal unless re-approved) map to FROZEN on-chain.

6. Sequence management: the channel pool

Constraints: one transaction per source account per ledger; a failed transaction consumes its sequence number (and fee); txBAD_SEQ consumes nothing; an expired transaction can never apply once a closed ledger's close time passes its maxTime.

Design:

  • channel_accounts table: account, next_seq (persisted), state (FREE/LEASED/QUARANTINED), leased_by (blockchain transaction id), lease_expires_at.
  • Lease: SELECT … FOR UPDATE SKIP LOCKED picks a FREE channel, assigns seq = next_seq, sets next_seq = seq + 1, and persists the signed envelope + hash, in the same DB transaction. One in-flight transaction per channel at a time.
  • Release: when the transaction is in a closed ledger (success or failure; the sequence is consumed either way), the channel returns to FREE.
  • Expiry: if getTransaction(hash) is NOT_FOUND and the latest closed ledger's close time > maxTime, the envelope can never apply. The business intent gets a new attempt (new channel lease, new envelope), and the channel's next_seq is re-read from the chain before the channel is freed.
  • txBAD_SEQ (another writer used the channel, e.g. a mis-configured second environment): the channel is QUARANTINED, an incident opens, and the intent moves to a fresh channel. Nothing ever re-signs a transaction with a guessed sequence number.
  • Crash recovery: on start, every LEASED channel is resolved by its persisted hash before reuse. There are no in-memory sequence counters.
  • Multiple workers: leases are row locks, so any number of worker replicas is safe. The MVP still runs one active submitter (advisory lock, as in the siblings) for simpler reasoning, and the pool gives the parallelism.
  • Throughput: n channels → up to n transactions per ledger, each up to 100 operations. Pool size (default 8) and batching are tuned in Stage 25 from measurements.

Every signature on an envelope must be consumed by the ledger's signature check, which stops at the threshold. A human proposal therefore carries a minimal sufficient subset of the uploaded signatures (chosen by descending weight), never all of them. A third human signature on a 2-of-4 issuance would otherwise invalidate it (txBAD_AUTH_EXTRA). All uploads stay in proposal_signatures for the audit.

GOV_CHANNEL is used only for human multisig proposals (issuance, burn, refill, clawback, signer changes). Proposals are serialized: one pending proposal per GOV_CHANNEL, with a long but bounded maxTime (default 24 h; Q-07).

7. Transaction lifecycle (BlockchainOperation)

stateDiagram-v2
  [*] --> CREATED: business tx commits intent (outbox)
  CREATED --> SIGNED: channel leased, envelope built with timebounds, signed, persisted with hash
  CREATED --> AWAITING_SIGNATURES: human multisig proposal
  AWAITING_SIGNATURES --> SIGNED: signature weight ≥ threshold (verified locally)
  AWAITING_SIGNATURES --> EXPIRED: maxTime passed unsigned
  SIGNED --> SUBMITTED: sendTransaction PENDING / DUPLICATE
  SIGNED --> UNKNOWN: timeout or transport error
  SUBMITTED --> UNKNOWN: no result yet
  UNKNOWN --> SUBMITTED: resubmit identical envelope
  SUBMITTED --> APPLIED: getTransaction SUCCESS
  SUBMITTED --> FAILED: getTransaction FAILED (in ledger; seq + fee consumed)
  UNKNOWN --> APPLIED
  UNKNOWN --> FAILED
  UNKNOWN --> EXPIRED: NOT_FOUND and closed ledger close time > maxTime (checked on ≥ 1 healthy node at a ledger ≥ maxTime)
  SIGNED --> REJECTED: sendTransaction ERROR (never entered the ledger; e.g. txBAD_SEQ, txINSUFFICIENT_FEE)
  EXPIRED --> [*]: intent gets a new attempt or fails per handler
  REJECTED --> RECONCILIATION_REQUIRED: txBAD_SEQ / unexpected
  APPLIED --> [*]
  FAILED --> [*]
  • Business intent (blockchain_operations) ↔ attempts (stellar_transactions, one per envelope). An intent has at most one non-terminal attempt.
  • Every envelope carries MEMO_HASH = SHA-256("dara/op/v1" ‖ intent id). The indexer links on-chain effects to intents through this memo, even when our database lost the response.
  • Time bounds: automatic transactions maxTime = now + 60 s (configurable); human proposals 24 h. maxTime = 0 is refused by the builder (D-E05). NTP drift is monitored; the expiry decision uses ledger close time, never the local clock.
  • Handlers (by purpose, inside the same DB transaction as the state change): OnSubmitted, OnApplied, OnFailed(reason), OnReconciliationRequired. There is no confirmed/finalized split: a closed ledger is final (D-E04).
  • Fees: fee = min(max_fee_per_op, max(100, p70 of getFeeStats inclusion fee)) × ops. During surge, an expired attempt may be rebuilt with a higher fee, up to the cap.
  • Errors are decoded into the domain taxonomy (brief §90): AccountMissing, TrustlineMissing, NotAuthorized, InsufficientBalance, InsufficientReserve, SequenceConflict, TransactionExpired, FeeTooLow, DestinationFull, OperationNotAllowed, NetworkUnavailable, UnknownSubmissionResult.

8. Flows mapped to Stellar

Domain operationStellarSupply effect
IssueAsset(asset, q, TREASURY_COLD)Payment op source ISSUER → TREASURY_COLD; human proposal+q
BurnAsset(asset, q)Payment op source TREASURY_COLD → ISSUER; human proposal−q
Refill hotPayment COLD → HOT; human proposal0
SweepPayment HOT → COLD; automatic above the ceiling0
BUY settlementPayment HOT → customer0
SELL settlementPayment customer → HOT (signed by CUSTODY)0
TRANSFERPayment customer → customer (signed by CUSTODY)0
Freeze / unfreezeSetTrustLineFlags (signed by AUTH)0
ClawbackClawback from customer; human proposal; re-issue to the rightful owner is a separate, approved issuance−q (then +q if re-issued)

BUY and SELL never touch the issuer, so INV "BUY/SELL/TRANSFER do not change supply" holds by construction and is also checked by reconciliation.

9. RPC architecture

  • Two self-hosted stellar-rpc v30 nodes (A, B), each with its own captive Core, on separate hosts (D-S01). Mainnet hardware: baseline §5.
  • Gateway health sample every 5 s per node: getHealth (status, latest ledger, ledger close-time age), getNetwork (passphrase must match), getLatestLedger (sequence + hash), latency, and error rate. States: HEALTHY, DEGRADED (lagging > 2 ledgers or slow), UNHEALTHY, DISABLED (operator).
  • Reads: the healthiest node, with failover. Critical reads (reconciliation) read ledger entries and record latestLedger from the response; reconciliation uses one node per run and records which.
  • Writes: the persisted envelope is sent to every healthy node. DUPLICATE counts as success. A different envelope is never built for the same attempt.
  • Expiry proof (EXPIRED) needs a healthy node whose latest ledger close time is past maxTime and which returns NOT_FOUND. With two healthy nodes, both must agree; otherwise the attempt stays UNKNOWN and the gate counts it.
  • No hosted RPC is configured. An optional read-only fallback is supported but disabled (independence).

10. Indexing and reconciliation

  • Indexer: Go Ingest SDK reading LedgerCloseMeta from our RPC (getLedgers), ledger by ledger. The cursor is the ledger sequence, so gaps are impossible inside the RPC window. It records, for our assets and accounts: payments (decoded as issue/burn/transfer), clawbacks, trustline creation/flag changes, signer and threshold changes on our system accounts, and memo hashes. Idempotent on (ledger, tx index, op index).
  • Retention: RPC keeps 7 days by default. If the indexer falls further behind, it backfills the gap from a SEP-54 ledger data lake. The source is our own Galexie lake on local disk (Q-10, decided; deploy/datalake/). The same Ingest SDK code reads it through ledgerbackend.BufferedStorageBackend (any SDK LedgerBackend plugs into the indexer's BackendSource), so there is one decoder and the cursor (ledger sequence) is unchanged. With no backfill source configured, the indexer stops at the gap and opens a CRITICAL INDEXER_GAP incident; it never skips a ledger. Reconciliation's own balance checks still work, because they read current ledger entries; its indexer_current check fails until the gap is filled.
  • Start and the ceremony: the indexer's first ledger is at or before the ceremony, so it sees every issuance. Signer/threshold/flag changes on an account in ledgers that closed before the platform registered (and verified) the account are its ceremony and open nothing; any later change without an intent that asks for it (customer onboarding sets the custody signer) is CRITICAL.
  • Unexpected-event detection (CRITICAL incidents): issuance or clawback without an approved proposal; signer/threshold/flag change on ISSUER, Treasury, sponsor or funder accounts without an approved proposal; any authorized trustline on an account we did not create; any payment that touches our accounts without a known memo; a claimable balance, liquidity pool or SAC contract balance in our asset; a change to home_domain. Deployment of the asset's SAC by anyone is a WARNING: it grants no powers, but the scanner should show it.
  • Supply = Σ trustline balances + claimable balances + liquidity-pool reserves + SAC contract balances (baseline §10). The closed holder set (Q-03) means only the first term should be non-zero. The other three are checked to be zero, not assumed. Anyone can deploy the SAC for a Classic asset without permission. The indexer records every claimable balance, liquidity-pool reserve and SAC contract balance (decoded with the SDK's ingest/sac helpers) in our assets, so reconciliation checks they are zero from observed state; it does not rely on the expectation that, under AUTH_REQUIRED, a contract balance starts unauthorized. (That expectation is still unverified on Quickstart; it only affects how likely a finding is, not whether it is detected.) Read with getLedgerEntries in batches of trustline keys for: HOT, COLD, every customer account. Cross-checks: (1) Σ on-chain = issued − burned − clawed back per indexer; (2) supply ≤ approved backing; (3) per-account on-chain balance = ledger position (minus in-flight attempts); (4) trustline count from the indexer = known accounts.
  • Implemented checks (reconciliation package): ledger integrity (balanced, projection = entries, no negative restricted balance); per asset, from one node at one ledger L: each known account's balance = its ledger position, Σ known balances = TOKEN_SUPPLY, supply ≤ unexpired backing, TOKEN_SUPPLY = Σ holder accounts, business trustline state = on-chain authorization (and clawback enabled), issuer layout = ceremony layout; from the indexer: issued − burned − clawed back = every holding it has seen, nothing held outside the trustlines of known accounts, indexer within 60 ledgers of L. In-flight intents are resolved per attempt hash against L (applied ≤ L but not recorded: the chain has it; recorded but applied after L: the ledger has it); a node that cannot prove either makes the affected accounts SKIPPED_IN_FLIGHT for that run, never a guess.
  • Details: the checks and what each failure means are in ../operations/RECONCILIATION_RUNBOOK.md; how ledger accounts map to on-chain balances is ../finance/LEDGER_MODEL.md §4.

11. Validator decision

Option A (self-hosted RPC with captive Core, ×2, no validator) is selected (D-S01 (4); baseline §11). We do not participate in SCP and have no quorum set to configure. If Stellar Mainnet is unreachable, the trading gate closes (fail closed, D-S01 (3)); there are no PENDING_CHAIN_SETTLEMENT trades.

12. Environments and safety

EnvNetworkRPCSigners
LOCAL / DEMO / TEST / CIQuickstart --local (own passphrase per run)Quickstart's RPCDev signer (deterministic seeds from a local file, never committed); refused unless the passphrase is a local one
TESTNETTestnetOwn RPC on Testnet (SDF public RPC allowed only as a read fallback)Testnet-only keys; human signers are test hardware keys
UATTestnetTwo own RPC nodesProduction signer adapter, test keys
MAINNET_CANARY / PRODUCTIONMainnetTwo own RPC nodesHSM/remote signer; human hardware keys

Startup verification: passphrase (config = getNetwork = manifest), the issuer's flags and signer set, the Treasury, sponsor and channel signer sets, and that the signer backend is allowed for that passphrase. The dev signer and the demo seed command refuse the Mainnet and Testnet passphrases.

در این صفحه