|
| 1 | +# Bridge Local Development Setup |
| 2 | + |
| 3 | +This document describes how to run a complete local bridge environment for development and |
| 4 | +testing — single-validator and multi-validator — using the Make targets provided in the |
| 5 | +repository root. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +| Tool | Minimum version | Notes | |
| 12 | +|---|---|---| |
| 13 | +| Go | 1.21 | `go version` | |
| 14 | +| Rust + Cargo | stable | via [rustup](https://rustup.rs/) | |
| 15 | +| Node.js + npm | 18 | `node --version` | |
| 16 | +| Internet | — | Stellar testnet (Friendbot + Horizon) | |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## Quick Start |
| 21 | + |
| 22 | +### Single-validator |
| 23 | + |
| 24 | +```bash |
| 25 | +make bridge-dev |
| 26 | +``` |
| 27 | + |
| 28 | +That's it. On first run this builds TFChain (Rust, ~20–40 min). Every subsequent run reuses |
| 29 | +the existing binary and takes ~1 min. |
| 30 | + |
| 31 | +### Multi-validator (3 daemons, 2-of-3 threshold) |
| 32 | + |
| 33 | +```bash |
| 34 | +make bridge-mv-dev |
| 35 | +``` |
| 36 | + |
| 37 | +Same one-shot command. Spins up 3 bridge daemons (Alice, Bob, Charlie), configures the bridge |
| 38 | +Stellar account as a 2-of-3 multi-sig, and runs the full MV test suite. |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## What `make bridge-dev` Does |
| 43 | + |
| 44 | +| Step | Target | What happens | |
| 45 | +|---|---|---| |
| 46 | +| 1 | `bridge-clean` | Kill any running bridge/TFChain processes, delete persistency files and logs | |
| 47 | +| 2 | `bridge-build` | `go build` the bridge binary (fast, ~5s) | |
| 48 | +| 3 | _(auto)_ | Build TFChain node if binary missing (`substrate-node/target/release/tfchain`) | |
| 49 | +| 4 | `bridge-accounts` | Generate fresh Stellar keypairs; fund via Friendbot; create TFT trustlines; issue TFT to bridge via `path_payment_strict_send` and to user via `payment`; write `/tmp/bridge_local_env.sh` | |
| 50 | +| 5 | `bridge-tfchain-start` | Start TFChain `--dev --tmp`; poll WS until node is ready | |
| 51 | +| 6 | `bridge-setup` | Register Alice as bridge validator; set bridge wallet, fee account, deposit/withdraw fees via sudo | |
| 52 | +| 7 | `bridge-start` | Start bridge daemon; wait for `bridge_started` log entry | |
| 53 | +| 8 | `bridge-test` | Run 4-scenario E2E test suite | |
| 54 | + |
| 55 | +TFChain is built once via Make's file-dependency model. If `substrate-node/target/release/tfchain` |
| 56 | +already exists, the Rust build step is skipped entirely. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## Individual Targets |
| 61 | + |
| 62 | +```bash |
| 63 | +# Build |
| 64 | +make bridge-build # Go build only (fast) |
| 65 | +make bridge-build-tfchain # Rust build (slow, one-time) |
| 66 | + |
| 67 | +# Environment lifecycle |
| 68 | +make bridge-accounts # (Re)generate Stellar accounts → /tmp/bridge_local_env.sh |
| 69 | +make bridge-tfchain-start # Start TFChain dev node |
| 70 | +make bridge-setup # Configure bridge pallet on TFChain |
| 71 | +make bridge-start # Start bridge daemon |
| 72 | +make bridge-stop # Stop bridge daemon |
| 73 | +make bridge-tfchain-stop # Stop TFChain node |
| 74 | +make bridge-clean # Stop everything + delete all local state |
| 75 | + |
| 76 | +# Testing |
| 77 | +make bridge-test # Run E2E tests against a running environment |
| 78 | +``` |
| 79 | + |
| 80 | +### Configuration overrides |
| 81 | + |
| 82 | +All targets accept environment variable overrides: |
| 83 | + |
| 84 | +```bash |
| 85 | +TFCHAIN_URL=ws://localhost:9944 \ # default |
| 86 | +BRIDGE_TFT_FLOAT=20000 \ # TFT issued to bridge wallet |
| 87 | +USER_TFT_AMOUNT=1000 \ # TFT issued to test user |
| 88 | +DEPOSIT_FEE=10000000 \ # 1 TFT (7 decimal places) |
| 89 | +WITHDRAW_FEE=10000000 \ # 1 TFT |
| 90 | +BRIDGE_ENV_FILE=/tmp/bridge_local_env.sh \ |
| 91 | + make bridge-dev |
| 92 | +``` |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +## Account Sharing Between Steps |
| 97 | + |
| 98 | +All scripts share account details via a single env file written by `make bridge-accounts`: |
| 99 | + |
| 100 | +``` |
| 101 | +/tmp/bridge_local_env.sh # single-validator |
| 102 | +/tmp/bridge_mv_env.sh # multi-validator |
| 103 | +``` |
| 104 | + |
| 105 | +Each subsequent script (`bridge_setup.js`, `bridge_tests.js`, etc.) calls `loadEnv()` at |
| 106 | +startup to read this file into `process.env`. The Makefile shell targets source it for |
| 107 | +`BRIDGE_SECRET`, `BRIDGE_ADDRESS`, etc. |
| 108 | + |
| 109 | +Re-running `make bridge-accounts` generates fresh Stellar keypairs and invalidates the current |
| 110 | +environment — you would need to re-run `bridge-setup` and `bridge-start` as well. The |
| 111 | +`make bridge-clean` + `make bridge-dev` cycle handles this automatically. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Logs |
| 116 | + |
| 117 | +```bash |
| 118 | +tail -f /tmp/bridge_local.log # bridge daemon |
| 119 | +tail -f /tmp/tfchain_local.log # TFChain node |
| 120 | + |
| 121 | +# Multi-validator |
| 122 | +tail -f /tmp/bridge_mv_1.log # Val1 (Alice) |
| 123 | +tail -f /tmp/bridge_mv_2.log # Val2 (Bob) |
| 124 | +tail -f /tmp/bridge_mv_3.log # Val3 (Charlie) |
| 125 | +``` |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## Tests |
| 130 | + |
| 131 | +### Single-validator tests (`make bridge-test`) |
| 132 | + |
| 133 | +| Test | Description | Expected outcome | |
| 134 | +|---|---|---| |
| 135 | +| 1 | Normal withdraw | Swap 2 TFT on TFChain → receive 1 TFT on Stellar (1 TFT fee) | |
| 136 | +| 2 | Batch withdraws | 5 simultaneous swaps in one block → all 5 delivered | |
| 137 | +| 3 | Bad deposit | Send TFT to bridge without memo → full refund on Stellar | |
| 138 | +| 4 | Crash recovery | SIGKILL bridge mid-withdraw → restart → delivery completes | |
| 139 | + |
| 140 | +### Multi-validator tests (`make bridge-mv-test`) |
| 141 | + |
| 142 | +| Test | Description | Expected outcome | |
| 143 | +|---|---|---| |
| 144 | +| MV1 | Normal withdraw | 3 validators, threshold=2; 1 TFT delivered | |
| 145 | +| MV2 | Deposit/mint | All 3 validators propose; mint threshold met | |
| 146 | +| MV3 | Bad deposit | All 3 detect bad deposit; full refund delivered | |
| 147 | +| MV4 | Validator offline | Val3 killed; Val1+Val2 meet threshold=2; refund works; Val3 restarted after | |
| 148 | +| MV5 | Batch withdraws | 3 simultaneous burns; all 3 delivered (uses expiry recovery if sequence collision) | |
| 149 | + |
| 150 | +The test runner exits non-zero on any failure, making it composable with CI. |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## Multi-validator Setup Details |
| 155 | + |
| 156 | +### What `make bridge-mv-dev` does |
| 157 | + |
| 158 | +| Step | Target | What happens | |
| 159 | +|---|---|---| |
| 160 | +| 1 | `bridge-mv-clean` | Kill all 3 daemons, delete MV persistency files and logs | |
| 161 | +| 2 | `bridge-build` | Build bridge binary | |
| 162 | +| 3 | _(auto)_ | Build TFChain if binary missing | |
| 163 | +| 4 | `bridge-mv-accounts` | Generate 4 keypairs (val1=bridge, val2, val3, user); fund via Friendbot; create trustlines; fund bridge via `path_payment_strict_send`; configure bridge as 2-of-3 multi-sig (val1 master key, val2+val3 added as signers, thresholds: low=1, med=2, high=3); write `/tmp/bridge_mv_env.sh` | |
| 164 | +| 5 | `bridge-tfchain-start` | Start TFChain dev node | |
| 165 | +| 6 | `bridge-mv-setup` | Create twins for Alice, Bob, Charlie; register all 3 as validators; set bridge wallet, fee account, fees | |
| 166 | +| 7 | `bridge-mv-start` | Start 3 bridge daemons; wait for all 3 to log `bridge_started` | |
| 167 | +| 8 | `bridge-mv-test` | Run MV1–MV5 test suite | |
| 168 | + |
| 169 | +### Multi-sig architecture |
| 170 | + |
| 171 | +``` |
| 172 | +Bridge Stellar account = Val1's keypair (master key, weight=1) |
| 173 | +Val2 keypair added as signer (weight=1) |
| 174 | +Val3 keypair added as signer (weight=1) |
| 175 | +
|
| 176 | +Thresholds: |
| 177 | + low = 1 (any 1 of 3 can change account options) |
| 178 | + med = 2 (any 2 of 3 must sign TFT payment transactions) |
| 179 | + high = 3 (all 3 must sign for account deletion etc.) |
| 180 | +``` |
| 181 | + |
| 182 | +Each bridge daemon signs with its own validator Stellar key and stores its signature on TFChain. |
| 183 | +When `BurnTransactionReady` or `RefundTransactionReady` fires, the first validator to receive it |
| 184 | +fetches all stored signatures from TFChain and builds a multi-sig Stellar transaction meeting |
| 185 | +the threshold. |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## Fee Mechanics |
| 190 | + |
| 191 | +TFT uses 7 decimal places: `1 TFT = 10,000,000 base units`. |
| 192 | +Default fees are 1 TFT each (configurable via `DEPOSIT_FEE` and `WITHDRAW_FEE`). |
| 193 | + |
| 194 | +### Deposit (Stellar → TFChain) |
| 195 | + |
| 196 | +Two enforcement layers: |
| 197 | + |
| 198 | +1. **Bridge** (`mint.go`): if incoming Stellar amount ≤ `DepositFee`, bridge refunds on Stellar |
| 199 | + without proposing a mint (avoids an on-chain tx that would fail anyway). |
| 200 | +2. **Pallet** (`execute_mint_transaction`): deducts `DepositFee` and mints `amount - deposit_fee` |
| 201 | + to the user's TFChain account. |
| 202 | + |
| 203 | +### Withdraw (TFChain → Stellar) |
| 204 | + |
| 205 | +One enforcement layer: |
| 206 | + |
| 207 | +1. **Pallet** (`swap_to_stellar`): rejects if `amount ≤ WithdrawFee` with |
| 208 | + `AmountIsLessThanWithdrawFee`. If valid, stores `burn_amount = amount - withdraw_fee` in the |
| 209 | + event. The bridge reads this value directly and sends it to Stellar — no additional fee applied. |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +## Bridge Funding: Why `path_payment_strict_send` |
| 214 | + |
| 215 | +The bridge Stellar deposit monitor only watches `payment` operations on the bridge account. |
| 216 | +A `path_payment_strict_send` operation is structurally different and is not picked up by the |
| 217 | +monitor — so funding the bridge wallet this way does not trigger a spurious refund on startup |
| 218 | +rescan. Always use `path_payment_strict_send` when issuing TFT to the bridge account in test |
| 219 | +setup. |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +## Idempotency and Crash Recovery |
| 224 | + |
| 225 | +The bridge writes an idempotency record (bbolt DB at `<persistency>.idem.db`) before submitting |
| 226 | +any Stellar transaction: |
| 227 | + |
| 228 | +- `PROCESSING`: Stellar tx may or may not have been submitted |
| 229 | +- `COMPLETED`: Stellar tx submitted and TFChain confirmation done |
| 230 | + |
| 231 | +On restart, `reconcilePendingTransactions` scans all `PROCESSING` entries. For each: |
| 232 | + |
| 233 | +1. Fetch recent outgoing Stellar transactions from Horizon (single request, reused for all checks) |
| 234 | +2. Search by memo text (primary) then by sequence number (fallback) |
| 235 | +3. If found: proceed directly to TFChain confirmation — no double-spend |
| 236 | +4. If not found: log `"no Stellar tx found by memo or sequence, safe to retry"` — re-submit on |
| 237 | + next Ready event |
| 238 | + |
| 239 | +--- |
| 240 | + |
| 241 | +## Known Limitations |
| 242 | + |
| 243 | +1. **Sequence coordination under load**: All validators sign Stellar transactions at proposal time |
| 244 | + with a fixed sequence number. If another Stellar transaction from the bridge account is |
| 245 | + submitted between proposal and `Ready` events, the stored signatures reference a stale |
| 246 | + sequence → all validators get `tx_bad_seq` on first attempt. Recovery is automatic via the |
| 247 | + expiry cycle (~20 blocks, ~2 min delay). This is a pre-existing design constraint, not |
| 248 | + introduced by this PR. |
| 249 | + |
| 250 | +2. **Stellar testnet Friendbot rate limits**: If Friendbot rejects account funding (rate limited |
| 251 | + or account already exists with funds), re-running `make bridge-accounts` generates fresh |
| 252 | + keypairs. This requires re-running `make bridge-setup` and `make bridge-start` as well — |
| 253 | + or simply re-run `make bridge-dev` for a clean slate. |
| 254 | + |
| 255 | +3. **`--tmp` chain**: State is lost on TFChain restart. For persistent sessions, replace |
| 256 | + `--tmp` with `--base-path /tmp/tfchain-data` in the `bridge-tfchain-start` Makefile target. |
0 commit comments