Skip to content

Commit 507046d

Browse files
sameh-faroukclaude
andcommitted
feat(bridge): batch withdraw/refund proposals (force_batch API + E2E harness)
Batching-focused extraction of the remaining unique work from the old #1079 draft, rebuilt cleanly on current development. Contains the batching API layer (unwired) and the local E2E test harness; the handler integration is intentionally NOT included — it must be rebuilt on top of the per-tx idempotency work (#1096) when this is picked up. Batching API: - clients/tfchain-client-go/batch.go: BatchCalls — submits N calls in a single Utility.force_batch extrinsic (continues through per-item failures, unlike batch/batch_all). Returns a BatchResult. - pkg/substrate/client.go: BurnProposal/RefundProposal + BatchProposeAll (batched propose) and BatchSetWithdrawExecuted/BatchSetRefundTransaction- Executed (batched confirm), each with single-item + whole-batch fallback to the existing Retry* paths. - pkg/substrate/events.go: populate the (already-declared) RefundCreatedEvents so the batch proposer can see RefundTransactionCreated events. Local E2E test harness (scripts/ + Makefile + docs): - scripts/bridge_*.js + wait_for_node.js: single- and multi-validator (2-of-3) end-to-end scenarios incl. crash recovery, double-spend, and backlog-drain. - Makefile: bridge-dev / bridge-test / bridge-mv-* targets to spin up TFChain + bridge(s) and run the suite. - bridge/docs/local_development_setup.md. KNOWN FOLLOW-UPS before this is production-ready (from review of #1079): 1. force_batch ItemFailed carries no index — re-derive true per-item outcomes (IsBurnedAlready / signature presence) instead of trusting the aggregate count, or use Utility.batch + BatchInterrupted.Index. 2. Failure count is whole-block, phase-unfiltered — filter Utility_ItemFailed by the batch extrinsic's phase (mirror checkForError) and clamp. 3. No size cap — chunk calls into bounded sub-batches (avoid block-weight cliff on backlog drain). 4. Wire the handler integration on top of #1096's per-tx submit, and add Go unit tests for BatchCalls / the proposal mapping. Motivating issues: #1053 (Ready-processing delays / accumulating txs), #1054 (bridge atomicity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a08dd29 commit 507046d

17 files changed

Lines changed: 9633 additions & 419 deletions

Makefile

Lines changed: 323 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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

Comments
 (0)