Skip to main content

DESIGN.md: Exchange8:Settlement — Detailed Design Document

1. Scope & Context​

Owns

  • Novation records (settlement.novation) — per-trade CCP substitution, idempotent per (env_id, trade_id), with a dense novation_seq.
  • CCP facility routing (settlement.ccp_facility, settlement.ccp_facility_file, settlement.security_to_ccp, settlement.clearing) — endpoint configuration, per-file-family credential refs and schedules, security→CCP mapping, participant clearing assignment.
  • Custody (settlement.asset, settlement.custody_account, settlement.custody_transaction) — crypto asset master, per-env custody addresses, on-chain transaction ledger with the pending → signed → broadcast → confirmed / failed state machine.
  • Collateral (settlement.collateral) — balances, haircuts, concentration buckets, rehypothecation flag.
  • Give-up allocations (settlement.giveup) and block/EFP/EFR trade reports (settlement.block_trade).
  • The FX conversion core and options exercise/assignment/expiry pure cores (their persistent position/contract tables belong to Exchange8:RefData (exchange8.fx_pair, exchange8.fx_tenor) and the contract master).
  • CCP EOD file exports (CDS, SIS, Fortis) and the settlement-netting CSV (reconciliation backstop), scheduled by Sentin8:Cron.
  • Audit journaling and the outbox producer for all of the above.

Does NOT own (delegates)

  • The event-sourced trade/position ledger, statements, novation of internal records — Exchange8:Clearing.
  • Matching and margin calculation — Exchange8:ME / Exchange8:Margin.
  • Regulatory reporting — Exchange8:Regulatory.
  • Fiat currency movement — Bank8:Ledger postings (this service computes the leg shapes; Ledger executes them).
  • Contract/position master tables — Exchange8:RefData / contract-ledger owners.
  • Cron triggers for expiry and EOD jobs — Sentin8:Cron.

System context​

Container view​

2. Runtime Architecture​

The binary (src/main.rs) is a Sentin8 Shell assembly: Sentin8Shell::new( "exchange8:settlement", "EXCHANGE8_SETTLEMENT") with registration printing, with_simple_state(), and with_router(extend_router) that mounts the policy-admin API. The global allocator is mimalloc (#[global_allocator]), covering all in-process tokio workers. Boot, DB pool, drain, relay and audit pusher are shell-provided.

Request family: novation​

Request family: custody movement​

Request family: give-up allocation​

Request family: block trade report​

Request family: FX conversion (spot / forward / NDF)​

Request family: options exercise / expiry​

Request family: CCP file ingest/export​

3. Data Model​

Schema-per-service: database exchange8, schema settlement, role exchange8_settlement_service, DSN env EXCHANGE8_SETTLEMENT_DATABASE_URL. RLS (ENABLE + FORCE) on every table; policies require sentin8_context_initialized() and env_id = sentin8_current_env_id() (set via SELECT sentin8_set_tenant_context(...) on the same connection).

Per-table notes:

TableVisibilityNotes
settlement.clearingenv-scoped, RLS FORCEparticipant clearing assignment, status ∈ {E, D}; UNIQUE (env_id, market_code, connection_id, clearing_id)
settlement.ccp_facilityenv-scopedCCP endpoint config; credentials live on the child *_file rows as secret refs
settlement.ccp_facility_fileenv-scopedfile_type = CDS / SIS / Fortis family; schedule feeds Sentin8:Cron
settlement.security_to_ccpenv-scopedwhich CCP clears which security
settlement.novationenv-scopedidempotency contract: UNIQUE (env_id, trade_id)
settlement.assetenv-scopedcrypto asset master (chain, contract address, decimals)
settlement.custody_accountenv-scopedaddress + balance; creds_secret_ref only
settlement.custody_transactionenv-scopedon-chain ledger; index (env_id, state) for stuck-tx sweep
settlement.collateralenv-scopedindex (env_id, account_id)
settlement.giveupenv-scopedallocation awaiting clearing-broker acceptance
settlement.block_tradeenv-scopedreported negotiated trades with validation state
settlement.outboxenv-scopedDBX §6.7 producer table; partial index on relayed = false
settlement.audit_journalenv-scopedDBX §6.8 journal; partial index on pushed = false

Lifecycles​

4. API Surface​

Grouped by audience; the authorization gate for all handler surfaces is the 8-step platform pipeline — bearer verify → tenant resolve → feature gate → Shield (exchange8:settlement-policy scope-mirror, enforced) → RLS context → execute → audit. The HTTP router mounts health, /openapi.json and the Shield policy-admin API only; domain triggers arrive via events and Cron.

AudienceSurfaceGateEmits
System (M2M)Novation of trade events (exchange8:settlement:run action)scope-mirror permit / sentin8:sys admin scopesexchange8:clearing:trade:novated (vertical design §4.1)
System (M2M)Custody movements (exchange8:custody:write)step-up gate for withdrawals + scope-mirroroutbox event (see §10)
System (M2M)Give-up allocation / block validation (exchange8:giveup:write, exchange8:blocktrade:write)scope-mirror permitoutbox event (see §10)
System (M2M)FX legs + exposure verdicts (exchange8:settlement:write)scope-mirror permitoutbox event (see §10)
System (M2M)Exercise / assignment / expiry (exchange8:novation:write)scope-mirror permitexchange8:clearing:exercise:requested, exchange8:clearing:assignment:allocated, exchange8:clearing:expiry:settled
Admin (RFW)/v1/admin/ui/novation + POST /v1/admin/novation (page Novation)exchange8:novation:read / :write—
Admin (RFW)/v1/admin/ui/custody + POST /v1/admin/custody (page Custody)exchange8:custody:read / :write—
Admin (RFW)/v1/admin/ui/collateral + POST /v1/admin/collateralexchange8:collateral:read / :write—
Admin (RFW)/v1/admin/ui/blocks + POST /v1/admin/blocks (page Block Trades)exchange8:blocktrade:read / :write—
AdminShield policy-admin API (exchange8:settlement:policy:read / :write)role exchange8:settlement:adminsentin8:sys:policy family
OpsGET /v1/health, /openapi.jsonunauthenticated (health), none—

Declared error codes: exchange8_novation_failed (500), exchange8_block_trade_rejected (422).

Multi-party settlement chain (outbox → Bus → trigger):

5. Cross-Cutting Concerns​

  • Authentication — Sentin8:Auth is the only issuer. User access uses user tokens; system-driven novation/collateral/expiry use cert-bound M2M client_credentials tokens (DCR-registered per workload: exchange8:settlement, exchange8:settlement-policy, settlement-audit, exchange8-settlement-tls). DCR is fail-closed: only workloads present in sentin8_service_workloads (seeded by sentin8-cli vertical enroll) can register.
  • Authorization — the scope-mirror policy set (origin='platform', mode='enforced') permits each declared action for callers holding the matching scope or the platform-admin scopes (sentin8:sys:admin, sentin8:sys:org:admin, sentin8:sys:env:admin). No-set → deny (fail-closed); shadow is an emergency rollback lever only; token_tp never bypasses policy.
  • Tenant resolution + RLS — every table has ENABLE + FORCE ROW LEVEL SECURITY with policies sentin8_context_initialized() AND env_id = sentin8_current_env_id(); the context is set with SELECT sentin8_set_tenant_context(...) (set_config(..., true), transaction-local) on the same connection as the data statements — the async helper must be awaited or RLS silently sees no context. Explicit env_id = $N predicates remain the primary defense; RLS is the backstop. Role exchange8_settlement_service carries no BYPASSRLS.
  • Idempotency — novation: UNIQUE (env_id, trade_id) + engine re-entry; custody: tx_ref dedup → CustodyOutcome::Duplicate; batch relay: idempotency_key = outbox row id at Bus POST /v1/produce; settlement batches: net-then-check atomically.
  • Rate limiting / bot protection — ingress tier is the platform edge (Sentin8:Proxy); service-local rate policies ride the Shield policy surface (per-tenant sets), not bespoke code.
  • Audit journaling — every state transition writes settlement.audit_journal (same transaction where possible); the pusher drains to POST /v1/audit; Sys outage retries without loss.
  • Drain — sentin8::shell graceful drain on upgrade: HTTP stops accepting, in-flight requests finish, the relay/journal pusher checkpoint unrelayed/unpushed rows, and Sentin8:Captain's upgrade-instance swaps the binary (rolling, zero-downtime).
  • TLS — terminated in-process by sentin8-tls behind Sentin8:Proxy (single ingress, SNI exchange8-settlement.sys.ops…); the exchange8-settlement-tls workload resolves host context DB-free via GET /v1/resolve/hostname (sentin8:sys:instances:read). Post-quantum only; no classical fallback.
  • Allocator — mimalloc global allocator covers HTTP, relay, journal pusher and Cron-triggered workers alike.

6. Failure Modes & Recovery​

Instruction retry after a settlement-outcome failure​

Relay / journal retry and DLQ​

Recovery sequence (crash between write and relay):

FailureBehavior
CCP unreachablenovation rows persist; trades queue pending in the engine; EOD files re-render as the reconciliation backstop
Custody tx stuckstate machine sweep on (env_id, state) index → failed + alert
Collateral shortfallvaluation shortfall surfaced to Exchange8:Margin via events
Give-up rejectedno allocation; trade stays with the executing broker
Block report invalidstate ∈ {too_small, off_market, late} persisted, error exchange8_block_trade_rejected
Relay outageoutbox rows retry; idempotency_key makes replay safe
Sys outageaudit journal retries, never drops

7. Concurrency & Performance Model​

  • Task layout — one HTTP acceptor; one relay task (LISTEN settlement_outbox_changed + periodic unrelayed poll); one audit pusher; Cron-triggered jobs (EOD file render, options expiry) spawn as short-lived tasks. No cross-task shared mutable state in the cores: engines are per-trigger instances fed by projections, so no mutex contention on the hot path.
  • Pool sizing — the shared sentin8_config::pg_pool::connect caps connections (default 3, overridable) so ~20 services cannot exhaust PostgreSQL. Relay and journal use the same pool; context is set transaction-locally per statement set.
  • Hot paths — novation insert (single UNIQUE-guarded row + outbox row); exposure check (pure in-memory bucket math); file render (pure string building). Cache: nothing is hot-cached beyond the policy engine set (Shield hot-reload consumer keeps the loaded sets fresh).
  • Backpressure — outbox partial index bounds the relay scan; unrelayed rows retry with backoff so a Bus outage does not spin; the drain path checkpoints before shutdown.

8. Invariants, Insecure-Defaults Rejected, Unsafe Rationale​

Invariants (always true):

  1. Novation is idempotent per (env_id, trade_id) — a re-delivery never creates a second row or advances novation_seq.
  2. A domain write and its settlement.outbox (and wherever possible settlement.audit_journal) row are committed in the same transaction.
  3. Tenant context and data statements always run on the same connection; sentin8_set_tenant_context is awaited.
  4. Custody movements dedupe on tx_ref before funds move; a failed first application still blocks a duplicate retry's second apply.
  5. Settlement batches are all-or-nothing: one under-covered deliver leg rejects the batch; intra-batch netting never touches external funds.
  6. Crypto prices/quantities are scale-pinned (8, 8) and equity (4, 0); mixed-scale notionals are rejected, never approximated.
  7. NDF settlement produces exactly one quote-currency amount; deliverable forwards produce exactly two legs at the contract rate.
  8. Options physical delivery derives from the pre-exercise net position; assignments conserve the exercised quantity exactly.
  9. CCP export records match the pinned per-record byte lengths (CDS r1 = 80, r2 = r3 = 81; EOD records end # + LF); netting CSV always includes a header row.
  10. No plaintext private keys, custodian credentials or file credentials — only *_secret_ref values; no role carries BYPASSRLS.

Insecure defaults rejected:

  • danger_accept_invalid_certs / hand-rolled CA loading — TLS is in-process via sentin8-tls (post-quantum, root CA in the OS trust store).
  • Direct cross-schema/table joins into other services — cross-service data flows are membus/bus/owner-API per the access-order rule (this service declares zero cross-schema grants today).
  • Static superuser service DSN — the service role is unprivileged, non-bypass, database-exchange8-only.
  • Plaintext wallet keys in process — design rejects the pattern; adapters reference secrets.
  • Per-leg settlement fallback for crypto batches — an atomicity compromise rejected in favor of all-or-nothing batches.

No unsafe blocks exist in this service; the unsafe-rationale section is vacuously satisfied. Soundness-relevant safety instead rests on the invariants above (scale pins, tx_ref dedup, same-connection context).

9. Testing Strategy​

  • Unit/property layer — pure cores own all domain math: novation idempotency, pending-queue recovery order, collateral caps, give-up gates, block verdicts, exposure bucket/total limit precedence (strict > — at-limit is compliant), scale-pair guards, custody dedup, batch atomicity, withdrawal step-up/min/cap, assignment conservation, expiry threshold behavior, and CCP record byte-length assertions (the wire contract).
  • Policy layer — proves the real seeded scope-mirror text evaluates fail-closed, respects mode and origin, isolates sets per env_id, keeps M2M parity, and dry-runs clean at enroll.
  • SQL matrix — live PostgreSQL via the non-bypass exchange8_probe role: RLS enforcement (no context → 0 rows, own env → own rows, cross env → 0 rows) and the service-role catalog check. Skips silently when the DB is unreachable so test runs stay hermetic.
  • Deliberately not tested here — platform TLS/RLS plumbing itself, Sentin8:Bus delivery guarantees, Shield engine internals: covered by the platform's own test strategy docs.
  • E2E — the vertical's e2e-ts suite drives the real UI through BFF only (no direct SQL), so settlement surfaces are exercised as a human would.

10. Open Questions​

  • RFW admin pages (novation, custody, collateral, blocks) are registered but their data/UI handlers are not mounted in this binary — wiring the RFC 4.15 admin surface for these resources is outstanding.
  • settlement.outbox event emission from handlers (event types, stream families, trigger registrations in vertical-registry.json) is designed by the DBX pattern but not yet wired.
  • The exchange8:clearing:exercise:requested, exchange8:clearing:assignment:allocated and exchange8:clearing:expiry:settled event flow crosses Exchange8:Clearing's ledger and this engine — the final producer/consumer split between the two services is pending the clearing integration.
  • CCP transport (SFTP/HTTPS drop, per-facility schedules in ccp_facility_file.schedule) and acknowledgement/ingest handling are not yet implemented (the builders + netting CSV are).
  • Real CustodyAdapter implementations (custodian API credential flows, chain signers) — only the in-memory reference exists.
  • Corporate-action settlement flow (cash/stock delivery on corp-action events from Exchange8:RefData) is in scope per the vertical design but has no implementation here yet.
  • Greeks-based margin interplay (Exchange8:Margin SPAN vs portfolio margin for option positions) — the collateral rows here are haircut-based; the margin-model handshake is a margin-design decision.

Design attribution: exchange8:settlement and its underlying concepts were designed by Hamid Jahanzad Hamid@Jahanzad.com.