|
| 1 | +# 🏗️ Architecture & System Design |
| 2 | + |
| 3 | +> **StellarEscrow** — Decentralized escrow on Stellar/Soroban Network |
| 4 | +> Version: 1.0 | Audience: Engineers, DevOps, Technical Leadership |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +## Table of Contents |
| 9 | + |
| 10 | +- [System Overview](#1-system-overview) |
| 11 | +- [Blockchain Layer — Soroban Contract](#2-blockchain-layer--soroban-contract) |
| 12 | +- [Off-Chain Infrastructure](#3-off-chain-infrastructure) |
| 13 | +- [Networking & Security](#4-networking--security) |
| 14 | +- [Observability Stack](#5-observability-stack) |
| 15 | +- [Deployment Environments](#6-deployment-environments) |
| 16 | +- [Data Flow: Complete Trade Lifecycle](#7-data-flow-complete-trade-lifecycle) |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 1. System Overview |
| 21 | + |
| 22 | +StellarEscrow is a decentralized escrow platform built on the Stellar blockchain using Soroban smart contracts. It enables trustless peer-to-peer trades with USDC settlement, dispute arbitration, and tiered fee structures — all enforced by immutable on-chain logic. |
| 23 | + |
| 24 | +> **Key Design Principle:** All financial logic lives in the Soroban smart contract (immutable, on-chain). The off-chain stack (indexer, API, frontend) is stateless and reconstructible from blockchain event history at any time. |
| 25 | +
|
| 26 | +### 1.1 High-Level Architecture |
| 27 | + |
| 28 | +The platform is organized into four logical tiers: |
| 29 | + |
| 30 | +| Tier | Components | Role | |
| 31 | +|------|-----------|------| |
| 32 | +| **Blockchain** | Soroban WASM contract | Escrow state machine, USDC transfers, arbitration | |
| 33 | +| **Indexer** | Rust + Tokio service | Event polling, DB writes, WebSocket fanout | |
| 34 | +| **API** | Node.js gateway | REST endpoints, auth, analytics | |
| 35 | +| **Presentation** | SvelteKit + Nginx | Web UI, TLS termination, CDN headers | |
| 36 | + |
| 37 | +### 1.2 Component Inventory |
| 38 | + |
| 39 | +| Component | Technology | Language | Role | Port | |
| 40 | +|-----------|-----------|----------|------|------| |
| 41 | +| Smart Contract | Soroban / Stellar | Rust | Escrow state machine, USDC settlement | On-chain | |
| 42 | +| Indexer | Rust + Tokio | Rust | Event polling, DB writes, WebSocket fanout | 3000 (internal) | |
| 43 | +| API Gateway | Node.js | TypeScript | REST endpoints, auth, analytics | 4000 (internal) | |
| 44 | +| Client (SvelteKit) | SvelteKit + Vite | TypeScript | Web UI — trade lifecycle, funding | 3001 | |
| 45 | +| Nginx | nginx:alpine | — | TLS termination, reverse proxy, CDN headers | 80, 443 | |
| 46 | +| PostgreSQL 15 | postgres:15-alpine | SQL | Indexed event & trade storage | 5432 (internal) | |
| 47 | +| Redis 7 | redis:7-alpine | — | API response caching, rate-limit counters | 6379 (internal) | |
| 48 | +| Prometheus | prom/prometheus | — | Metrics scraping & time-series storage | 9090 (internal) | |
| 49 | +| Grafana | grafana/grafana | — | Dashboards & alert visualization | 3002 (internal) | |
| 50 | +| Loki + Promtail | grafana/loki | — | Centralized log aggregation | 3100 (internal) | |
| 51 | +| Alertmanager | prom/alertmanager | — | Alert routing (Slack, PagerDuty) | 9093 (internal) | |
| 52 | +| Certbot | certbot/certbot | — | Automated Let's Encrypt TLS renewal | — | |
| 53 | +| Backup Service | postgres:15-alpine | Bash | Daily pg_dump to S3 with retention | — | |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +## 2. Blockchain Layer — Soroban Contract |
| 58 | + |
| 59 | +### 2.1 Contract Architecture |
| 60 | + |
| 61 | +The escrow contract is the authoritative source of truth for all trade state. Deployed as a WASM binary to the Stellar Soroban environment, it is **immutable after deployment** — upgrades require migrating to a new contract address. |
| 62 | + |
| 63 | +#### Core Contract Functions |
| 64 | + |
| 65 | +| Function | Actor | Description | |
| 66 | +|----------|-------|-------------| |
| 67 | +| `create_trade(seller, amount, asset)` | Seller | Initializes escrow, sets terms, emits `created` event | |
| 68 | +| `get_funding_preview(trade_id, buyer)` | Buyer | Returns `FundingPreview`: balance, allowance, fee breakdown | |
| 69 | +| `execute_fund(trade_id, buyer, preview)` | Buyer | Transfers USDC into escrow, emits `funded` event | |
| 70 | +| `confirm_receipt(trade_id)` | Buyer | Releases escrowed funds to seller, emits `completed` event | |
| 71 | +| `raise_dispute(trade_id, reason)` | Buyer/Seller | Locks funds, assigns arbitrator, emits `disputed` event | |
| 72 | +| `resolve_dispute(trade_id, winner)` | Arbitrator | Releases funds to winner, emits `resolved` event | |
| 73 | +| `analytics_query(params)` | Read-only | Returns on-chain metrics: volume, success rate, unique addresses | |
| 74 | + |
| 75 | +#### Trade State Machine |
| 76 | + |
| 77 | +Every trade progresses through the following states. Transitions are enforced by the contract and cannot be bypassed: |
| 78 | + |
| 79 | +``` |
| 80 | +CREATED → FUNDED → COMPLETED |
| 81 | + ↘ |
| 82 | + DISPUTED → RESOLVED |
| 83 | +``` |
| 84 | + |
| 85 | +| State | Description | |
| 86 | +|-------|-------------| |
| 87 | +| `CREATED` | Seller has initialized the trade; awaiting buyer funding | |
| 88 | +| `FUNDED` | Buyer has deposited USDC; funds held in escrow | |
| 89 | +| `COMPLETED` | Buyer confirmed receipt; funds released to seller | |
| 90 | +| `DISPUTED` | One party raised a dispute; funds locked pending arbitration | |
| 91 | +| `RESOLVED` | Arbitrator decision executed; trade closed | |
| 92 | + |
| 93 | +### 2.2 Fee Structure |
| 94 | + |
| 95 | +StellarEscrow uses tiered fees based on cumulative trade volume per address: |
| 96 | + |
| 97 | +| Tier | Cumulative Volume | Fee Rate | |
| 98 | +|------|------------------|----------| |
| 99 | +| Standard | < $10,000 | 1.50% | |
| 100 | +| Silver | $10,000 – $100,000 | 1.00% | |
| 101 | +| Gold | $100,000 – $1,000,000 | 0.75% | |
| 102 | +| Platinum | > $1,000,000 | 0.50% | |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## 3. Off-Chain Infrastructure |
| 107 | + |
| 108 | +### 3.1 Indexer Service |
| 109 | + |
| 110 | +The Rust-based indexer bridges the Stellar blockchain to PostgreSQL. It polls the Stellar Horizon API at configurable intervals (default **5 seconds**, matching ledger close time), processes contract events, and writes normalized records to the database. It also serves as a WebSocket server for real-time frontend subscriptions. |
| 111 | + |
| 112 | +#### Indexer Configuration (`config.toml`) |
| 113 | + |
| 114 | +```toml |
| 115 | +[database] |
| 116 | +max_connections = 10 # Pool ceiling; increase for high-concurrency |
| 117 | +min_connections = 2 # Warm connections always available |
| 118 | + |
| 119 | +[stellar] |
| 120 | +poll_interval_seconds = 5 # Matches Stellar ledger close time |
| 121 | +network = "mainnet" # testnet | mainnet |
| 122 | + |
| 123 | +[cache] |
| 124 | +redis_url = "redis://:${REDIS_PASSWORD}@redis:6379" |
| 125 | +``` |
| 126 | + |
| 127 | +#### Redis Caching Strategy |
| 128 | + |
| 129 | +| Endpoint Pattern | TTL | Rationale | |
| 130 | +|-----------------|-----|-----------| |
| 131 | +| `GET /events*` | 10s | High-frequency reads; matches 5s Stellar ledger interval | |
| 132 | +| `GET /search*` | 30s | Search results change infrequently | |
| 133 | +| `GET /stats` | 60s | Aggregate queries are expensive; staleness acceptable | |
| 134 | +| `POST /events/replay` | No cache | Mutating operation — always hits database | |
| 135 | + |
| 136 | +> **Fallback:** If Redis is unavailable, all requests fall through to PostgreSQL with no errors. Cache misses degrade performance gracefully rather than causing failures. |
| 137 | +
|
| 138 | +### 3.2 API Gateway |
| 139 | + |
| 140 | +The Node.js API gateway exposes REST and WebSocket endpoints to the SvelteKit client and authenticated third-party consumers. It validates API keys (`API_KEYS` for standard access, `ADMIN_KEYS` for administrative endpoints), proxies analytics queries to the indexer, and serves trade history with CSV export. |
| 141 | + |
| 142 | +#### Key Environment Variables |
| 143 | + |
| 144 | +| Variable | Required | Description | |
| 145 | +|----------|----------|-------------| |
| 146 | +| `DATABASE_URL` | Yes | PostgreSQL connection string (DSN format) | |
| 147 | +| `INDEXER_URL` | Yes | Internal URL of the indexer (`http://indexer:3000`) | |
| 148 | +| `API_KEYS` | Yes | Comma-separated list of valid API keys | |
| 149 | +| `ADMIN_KEYS` | Yes | Admin-level API keys for privileged endpoints | |
| 150 | +| `NODE_ENV` | Yes | Must be `"production"` in production deployments | |
| 151 | +| `REDIS_URL` | No | Optional; enables response caching if set | |
| 152 | + |
| 153 | +### 3.3 Data Layer |
| 154 | + |
| 155 | +#### PostgreSQL Schema (Key Tables) |
| 156 | + |
| 157 | +| Table | Description | Indexes | |
| 158 | +|-------|-------------|---------| |
| 159 | +| `trades` | One row per escrow trade; mirrors contract state | trade_id (PK), status, buyer, seller, created_at | |
| 160 | +| `events` | Raw contract events from Stellar ledger | event_type, trade_id, ledger_sequence, timestamp | |
| 161 | +| `analytics` | Pre-aggregated metrics snapshots | time_window, metric_name | |
| 162 | +| `arbitrators` | Registered dispute arbitrators | address (PK), active flag | |
| 163 | + |
| 164 | +#### Connection Pool Sizing |
| 165 | + |
| 166 | +> **Rule of thumb:** `max_connections = (2 × CPU cores) + effective_spindle_count` |
| 167 | +> For a 4-core host: set `max_connections = 10–15` |
| 168 | +
|
| 169 | +--- |
| 170 | + |
| 171 | +## 4. Networking & Security |
| 172 | + |
| 173 | +### 4.1 Network Topology |
| 174 | + |
| 175 | +Internal services (PostgreSQL, Redis, Prometheus, Grafana, Loki) are **bound to `127.0.0.1` only** — not reachable from outside the Docker network. Only Nginx (ports 80/443) and the client (port 3001) are publicly exposed. |
| 176 | + |
| 177 | +``` |
| 178 | +Internet |
| 179 | + │ |
| 180 | + ▼ |
| 181 | +[Nginx :80/:443] ← TLS termination |
| 182 | + │ |
| 183 | + ├──► [Client :3001] (SvelteKit UI) |
| 184 | + ├──► [Indexer :3000] (internal only) |
| 185 | + └──► [API :4000] (internal only) |
| 186 | + │ |
| 187 | + ├──► [PostgreSQL :5432] (internal only) |
| 188 | + └──► [Redis :6379] (internal only) |
| 189 | +``` |
| 190 | + |
| 191 | +> ⚠️ **Security Rule:** Database and cache ports (5432, 6379) are **NEVER** published to external interfaces. The `docker-compose.yml` comments these out explicitly. |
| 192 | +
|
| 193 | +### 4.2 TLS / SSL |
| 194 | + |
| 195 | +TLS is managed by Certbot (Let's Encrypt) with automatic renewal every 12 hours. Certificates are stored in the `letsencrypt` Docker volume and mounted read-only into Nginx. |
| 196 | + |
| 197 | +```bash |
| 198 | +# Certificate renewal (runs every 12 hours via docker entrypoint) |
| 199 | +certbot renew --webroot -w /var/www/certbot --quiet |
| 200 | + |
| 201 | +# Cron-based renewal fallback |
| 202 | +0 0,12 * * * /opt/stellarescrow/scripts/renew-certs.sh |
| 203 | +``` |
| 204 | + |
| 205 | +### 4.3 Container Security |
| 206 | + |
| 207 | +| Security Control | Applied To | Effect | |
| 208 | +|-----------------|-----------|--------| |
| 209 | +| `no-new-privileges:true` | indexer, api | Prevents privilege escalation via setuid binaries | |
| 210 | +| `read_only: true` | indexer, api | Container filesystem is read-only; `/tmp` is tmpfs | |
| 211 | +| `requirepass` (Redis) | redis | `REDIS_PASSWORD` required for all connections | |
| 212 | +| Internal-only bind | postgres, redis | Not reachable from host network | |
| 213 | +| `POSTGRES_PASSWORD` env | postgres | Mandatory via `?`-syntax — startup fails without it | |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## 5. Observability Stack |
| 218 | + |
| 219 | +### 5.1 Metrics Pipeline |
| 220 | + |
| 221 | +``` |
| 222 | +Indexer ──► Prometheus ──► Grafana (dashboards) |
| 223 | + └────► Alertmanager ──► Slack / PagerDuty |
| 224 | +``` |
| 225 | + |
| 226 | +#### Key Performance Indicators |
| 227 | + |
| 228 | +| Metric | Target | Alert Threshold | |
| 229 | +|--------|--------|----------------| |
| 230 | +| TTFB | < 200ms (good), < 800ms (acceptable) | > 1000ms for 5+ minutes | |
| 231 | +| Largest Contentful Paint | < 2.5s | > 4.0s | |
| 232 | +| DB query mean (hot paths) | < 10ms | > 50ms for 3+ minutes | |
| 233 | +| Redis cache hit rate | > 80% | < 60% for 10+ minutes | |
| 234 | +| Indexer memory usage | < 256MB | > 400MB | |
| 235 | +| API error rate (5xx) | < 0.1% | > 1% for 2+ minutes | |
| 236 | + |
| 237 | +### 5.2 Log Aggregation |
| 238 | + |
| 239 | +Promtail ships container and host logs to Loki. Logs are queryable via Grafana's Explore interface using LogQL. All services use structured JSON logging with `RUST_LOG=info` for Rust services. |
| 240 | + |
| 241 | +### 5.3 Web Vitals |
| 242 | + |
| 243 | +The frontend collects Core Web Vitals (TTFB, LCP, FID, CLS) and CDN performance metrics via `observeWebVitals()` and `observeCdnPerformance()`. These beacon to `POST /api/metrics` and appear in Grafana alongside infrastructure metrics. |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +## 6. Deployment Environments |
| 248 | + |
| 249 | +| Environment | Network | Contract | Purpose | |
| 250 | +|-------------|---------|----------|---------| |
| 251 | +| Development | Stellar Testnet | Dev contract (redeployable) | Local dev with `pnpm dev` / `cargo test` | |
| 252 | +| Staging | Stellar Testnet | Staging contract | Pre-production validation; mirrors production config | |
| 253 | +| Production | Stellar Mainnet | Immutable mainnet contract | Live traffic; RTO ≤ 2h, RPO ≤ 24h | |
| 254 | + |
| 255 | +Each environment has its own `.env` file (`.env.development`, `.env.staging`, `.env.production`) with environment-specific secrets and RPC endpoints. |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## 7. Data Flow: Complete Trade Lifecycle |
| 260 | + |
| 261 | +``` |
| 262 | +1. Seller calls create_trade() on Soroban contract |
| 263 | + └─► Contract stores trade on-chain, emits `created` event |
| 264 | +
|
| 265 | +2. Indexer detects `created` event (within 5s poll) |
| 266 | + └─► Writes trade record to PostgreSQL `trades` table |
| 267 | +
|
| 268 | +3. Buyer's browser calls GET /trades/:id via API |
| 269 | + └─► Redis cache miss → API queries PostgreSQL → response cached (10s) |
| 270 | +
|
| 271 | +4. Buyer calls get_funding_preview() on contract (read-only) |
| 272 | + └─► Receives FundingPreview with balance, allowance, fee breakdown |
| 273 | +
|
| 274 | +5. Buyer approves USDC allowance → calls execute_fund() |
| 275 | + └─► Contract transfers USDC into escrow, emits `funded` event |
| 276 | +
|
| 277 | +6. Indexer picks up `funded` event |
| 278 | + └─► Updates DB status to FUNDED |
| 279 | +
|
| 280 | +7. Buyer confirms receipt → calls confirm_receipt() |
| 281 | + └─► Funds released to seller, emits `completed` event |
| 282 | +
|
| 283 | +8. Indexer updates trade to COMPLETED |
| 284 | + └─► Analytics aggregated → Grafana dashboard refreshed |
| 285 | +``` |
| 286 | + |
| 287 | +--- |
| 288 | + |
| 289 | +*© 2026 StellarEscrow* |
0 commit comments