@hyperlane-xyz/rebalancer-sim 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +195 -0
- package/README.md +582 -0
- package/dist/BridgeMockController.d.ts +87 -0
- package/dist/BridgeMockController.d.ts.map +1 -0
- package/dist/BridgeMockController.js +300 -0
- package/dist/BridgeMockController.js.map +1 -0
- package/dist/KPICollector.d.ts +81 -0
- package/dist/KPICollector.d.ts.map +1 -0
- package/dist/KPICollector.js +239 -0
- package/dist/KPICollector.js.map +1 -0
- package/dist/MessageTracker.d.ts +82 -0
- package/dist/MessageTracker.d.ts.map +1 -0
- package/dist/MessageTracker.js +213 -0
- package/dist/MessageTracker.js.map +1 -0
- package/dist/RebalancerSimulationHarness.d.ts +72 -0
- package/dist/RebalancerSimulationHarness.d.ts.map +1 -0
- package/dist/RebalancerSimulationHarness.js +217 -0
- package/dist/RebalancerSimulationHarness.js.map +1 -0
- package/dist/ScenarioGenerator.d.ts +50 -0
- package/dist/ScenarioGenerator.d.ts.map +1 -0
- package/dist/ScenarioGenerator.js +326 -0
- package/dist/ScenarioGenerator.js.map +1 -0
- package/dist/ScenarioLoader.d.ts +18 -0
- package/dist/ScenarioLoader.d.ts.map +1 -0
- package/dist/ScenarioLoader.js +59 -0
- package/dist/ScenarioLoader.js.map +1 -0
- package/dist/SimulationDeployment.d.ts +20 -0
- package/dist/SimulationDeployment.d.ts.map +1 -0
- package/dist/SimulationDeployment.js +170 -0
- package/dist/SimulationDeployment.js.map +1 -0
- package/dist/SimulationEngine.d.ts +58 -0
- package/dist/SimulationEngine.d.ts.map +1 -0
- package/dist/SimulationEngine.js +302 -0
- package/dist/SimulationEngine.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/runners/NoOpRebalancer.d.ts +17 -0
- package/dist/runners/NoOpRebalancer.d.ts.map +1 -0
- package/dist/runners/NoOpRebalancer.js +28 -0
- package/dist/runners/NoOpRebalancer.js.map +1 -0
- package/dist/runners/ProductionRebalancerRunner.d.ts +22 -0
- package/dist/runners/ProductionRebalancerRunner.d.ts.map +1 -0
- package/dist/runners/ProductionRebalancerRunner.js +219 -0
- package/dist/runners/ProductionRebalancerRunner.js.map +1 -0
- package/dist/runners/SimpleRunner.d.ts +31 -0
- package/dist/runners/SimpleRunner.d.ts.map +1 -0
- package/dist/runners/SimpleRunner.js +286 -0
- package/dist/runners/SimpleRunner.js.map +1 -0
- package/dist/runners/SimulationRegistry.d.ts +46 -0
- package/dist/runners/SimulationRegistry.d.ts.map +1 -0
- package/dist/runners/SimulationRegistry.js +156 -0
- package/dist/runners/SimulationRegistry.js.map +1 -0
- package/dist/types.d.ts +637 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +158 -0
- package/dist/types.js.map +1 -0
- package/dist/visualizer/HtmlTimelineGenerator.d.ts +6 -0
- package/dist/visualizer/HtmlTimelineGenerator.d.ts.map +1 -0
- package/dist/visualizer/HtmlTimelineGenerator.js +1321 -0
- package/dist/visualizer/HtmlTimelineGenerator.js.map +1 -0
- package/dist/visualizer/index.d.ts +4 -0
- package/dist/visualizer/index.d.ts.map +1 -0
- package/dist/visualizer/index.js +3 -0
- package/dist/visualizer/index.js.map +1 -0
- package/package.json +62 -0
- package/src/BridgeMockController.ts +404 -0
- package/src/KPICollector.ts +304 -0
- package/src/MessageTracker.ts +312 -0
- package/src/RebalancerSimulationHarness.ts +325 -0
- package/src/ScenarioGenerator.ts +433 -0
- package/src/ScenarioLoader.ts +73 -0
- package/src/SimulationDeployment.ts +265 -0
- package/src/SimulationEngine.ts +432 -0
- package/src/index.ts +101 -0
- package/src/runners/NoOpRebalancer.ts +40 -0
- package/src/runners/ProductionRebalancerRunner.ts +289 -0
- package/src/runners/SimpleRunner.ts +382 -0
- package/src/runners/SimulationRegistry.ts +215 -0
- package/src/types.ts +878 -0
- package/src/visualizer/HtmlTimelineGenerator.ts +1341 -0
- package/src/visualizer/index.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# Rebalancer Simulation Harness
|
|
2
|
+
|
|
3
|
+
A fast, real-time simulation framework for testing Hyperlane warp route rebalancers against synthetic transfer scenarios.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This simulator helps answer questions like:
|
|
8
|
+
|
|
9
|
+
- Does the rebalancer respond correctly to liquidity imbalances?
|
|
10
|
+
- How quickly does the rebalancer restore balance after a traffic surge?
|
|
11
|
+
- What happens when bridge delays cause the rebalancer to over-correct?
|
|
12
|
+
- How do different rebalancer strategies compare on the same traffic pattern?
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ SimulationEngine │
|
|
19
|
+
│ Orchestrates scenario execution, rebalancer polling, KPI collection│
|
|
20
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
21
|
+
│ │ │
|
|
22
|
+
▼ ▼ ▼
|
|
23
|
+
┌───────────────┐ ┌────────────────┐ ┌─────────────────┐
|
|
24
|
+
│ Scenario │ │ Rebalancer │ │ BridgeMock │
|
|
25
|
+
│ Generator │ │ Runners │ │ Controller │
|
|
26
|
+
│ │ │ │ │ │
|
|
27
|
+
│ Creates │ │ SimpleRunner │ │ Simulates slow │
|
|
28
|
+
│ transfer │ │ (simplified) │ │ bridge delivery │
|
|
29
|
+
│ patterns │ │ Production │ │ with config- │
|
|
30
|
+
│ │ │ Rebalancer │ │ urable delays │
|
|
31
|
+
└───────────────┘ └────────────────┘ └─────────────────┘
|
|
32
|
+
│ │ │
|
|
33
|
+
└────────────────────┼────────────────────┘
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────────────┐
|
|
36
|
+
│ Multi-Domain Deployment │
|
|
37
|
+
│ │
|
|
38
|
+
│ Single Anvil instance │
|
|
39
|
+
│ simulating N "chains" │
|
|
40
|
+
│ via different domain IDs │
|
|
41
|
+
│ │
|
|
42
|
+
│ Each domain has: │
|
|
43
|
+
│ - Mailbox (instant) │
|
|
44
|
+
│ - WarpToken + Collateral │
|
|
45
|
+
│ - Bridge (delayed) │
|
|
46
|
+
└─────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Key Concepts
|
|
50
|
+
|
|
51
|
+
### Warp Token Mechanics
|
|
52
|
+
|
|
53
|
+
Understanding collateral flow is critical:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
User sends FROM chain A TO chain B:
|
|
57
|
+
- Chain A: User deposits collateral → WarpToken GAINS collateral
|
|
58
|
+
- Chain B: Recipient withdraws → WarpToken LOSES collateral
|
|
59
|
+
|
|
60
|
+
This is counterintuitive! Transfers TO a chain DRAIN its liquidity.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Two Message Paths
|
|
64
|
+
|
|
65
|
+
The simulator uses two different delivery mechanisms:
|
|
66
|
+
|
|
67
|
+
| Path | Mechanism | Delay | Use Case |
|
|
68
|
+
| -------------------- | ----------------------- | -------------------------- | ----------------------------------- |
|
|
69
|
+
| User transfers | MockMailbox | Configurable (default 0ms) | Simulates Hyperlane message passing |
|
|
70
|
+
| Rebalancer transfers | MockValueTransferBridge | Configurable (e.g., 500ms) | Simulates CCTP/bridge delays |
|
|
71
|
+
|
|
72
|
+
This separation is important because rebalancer transfers go through external bridges (CCTP, etc.) which have significant delays, while user transfers use Hyperlane's fast messaging.
|
|
73
|
+
|
|
74
|
+
### Message Tracking
|
|
75
|
+
|
|
76
|
+
| Component | Description |
|
|
77
|
+
| ---------------- | ------------------------------------------------------------ |
|
|
78
|
+
| `MessageTracker` | Off-chain tracking of pending Hyperlane messages with delays |
|
|
79
|
+
| `KPICollector` | Collects transfer/rebalance metrics and generates final KPIs |
|
|
80
|
+
|
|
81
|
+
### Rebalancer Runners
|
|
82
|
+
|
|
83
|
+
Two rebalancer implementations are available:
|
|
84
|
+
|
|
85
|
+
| Runner | Description | Use Case |
|
|
86
|
+
| ---------------------------- | ------------------------------------------------------ | ------------------------------- |
|
|
87
|
+
| `SimpleRunner` | Simplified rebalancer with weighted/minAmount strategy | Fast tests, baseline comparison |
|
|
88
|
+
| `ProductionRebalancerRunner` | Wraps actual `@hyperlane-xyz/rebalancer` CLI service | Production behavior validation |
|
|
89
|
+
|
|
90
|
+
## Directory Structure
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
typescript/rebalancer-sim/
|
|
94
|
+
├── src/
|
|
95
|
+
│ ├── BridgeMockController.ts # Bridge delay simulation
|
|
96
|
+
│ ├── KPICollector.ts # Metrics collection
|
|
97
|
+
│ ├── MessageTracker.ts # Message tracking
|
|
98
|
+
│ ├── RebalancerSimulationHarness.ts # Main entry point
|
|
99
|
+
│ ├── ScenarioGenerator.ts # Create synthetic scenarios
|
|
100
|
+
│ ├── ScenarioLoader.ts # Load from JSON files
|
|
101
|
+
│ ├── SimulationDeployment.ts # Anvil + contract deployment
|
|
102
|
+
│ ├── SimulationEngine.ts # Simulation orchestration
|
|
103
|
+
│ ├── types.ts # Consolidated types
|
|
104
|
+
│ ├── index.ts # Explicit exports
|
|
105
|
+
│ ├── runners/ # Rebalancer implementations
|
|
106
|
+
│ │ ├── SimpleRunner.ts # Simplified for testing
|
|
107
|
+
│ │ ├── ProductionRebalancerRunner.ts # Wraps production service
|
|
108
|
+
│ │ └── SimulationRegistry.ts # IRegistry impl
|
|
109
|
+
│ └── visualizer/ # HTML timeline generation
|
|
110
|
+
│ └── HtmlTimelineGenerator.ts
|
|
111
|
+
├── scenarios/ # Pre-generated scenario JSON files
|
|
112
|
+
├── results/ # Test results (gitignored)
|
|
113
|
+
├── scripts/
|
|
114
|
+
│ └── generate-scenarios.ts
|
|
115
|
+
└── test/
|
|
116
|
+
├── scenarios/ # Unit tests for scenario generation
|
|
117
|
+
├── utils/ # Test utilities (Anvil management)
|
|
118
|
+
└── integration/ # Full simulation tests
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Scenario File Format
|
|
122
|
+
|
|
123
|
+
Each scenario JSON is self-contained with metadata, transfers, and default configurations:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"name": "extreme-drain-chain1",
|
|
128
|
+
"description": "Tests rebalancer response when one chain is rapidly drained.",
|
|
129
|
+
"expectedBehavior": "95% of transfers go TO chain1, draining collateral...",
|
|
130
|
+
"duration": 10000,
|
|
131
|
+
"chains": ["chain1", "chain2", "chain3"],
|
|
132
|
+
"transfers": [...],
|
|
133
|
+
"defaultInitialCollateral": "100000000000000000000",
|
|
134
|
+
"defaultTiming": {
|
|
135
|
+
"bridgeDeliveryDelay": 500,
|
|
136
|
+
"rebalancerPollingFrequency": 1000,
|
|
137
|
+
"userTransferInterval": 100
|
|
138
|
+
},
|
|
139
|
+
"defaultBridgeConfig": {...},
|
|
140
|
+
"defaultStrategyConfig": {...},
|
|
141
|
+
"expectations": {
|
|
142
|
+
"minCompletionRate": 0.9,
|
|
143
|
+
"shouldTriggerRebalancing": true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Tests can use the defaults from JSON or override them for specific test needs.
|
|
149
|
+
|
|
150
|
+
## Running Simulations
|
|
151
|
+
|
|
152
|
+
### 1. Generate Scenarios (one-time)
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pnpm generate-scenarios
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Creates JSON files in `scenarios/` with various traffic patterns.
|
|
159
|
+
|
|
160
|
+
### 2. Run All Tests
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pnpm test
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Tests automatically detect if Anvil is available. If not installed, integration tests are skipped.
|
|
167
|
+
|
|
168
|
+
### 3. Select Rebalancers
|
|
169
|
+
|
|
170
|
+
By default, tests run with both rebalancers. Use the `REBALANCERS` env var to select:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Run with simplified rebalancer only (faster)
|
|
174
|
+
REBALANCERS=simple pnpm test
|
|
175
|
+
|
|
176
|
+
# Run with production rebalancer only
|
|
177
|
+
REBALANCERS=production pnpm test
|
|
178
|
+
|
|
179
|
+
# Run with both (default) - compare behavior
|
|
180
|
+
REBALANCERS=simple,production pnpm test
|
|
181
|
+
|
|
182
|
+
# Compare on specific scenario (recommended for debugging)
|
|
183
|
+
REBALANCERS=simple,production pnpm test --grep "extreme-drain"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 4. View Results
|
|
187
|
+
|
|
188
|
+
Test results are saved to `results/` directory (gitignored):
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# JSON results with KPIs
|
|
192
|
+
cat results/extreme-drain-chain1.json
|
|
193
|
+
|
|
194
|
+
# HTML timeline visualization
|
|
195
|
+
open results/extreme-drain-chain1-HyperlaneRebalancer.html
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Note:** If Anvil is not installed, integration tests will be skipped. Install Foundry with:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
curl -L https://foundry.paradigm.xyz | bash && foundryup
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Visualization
|
|
205
|
+
|
|
206
|
+
The simulator generates interactive HTML timelines for each test run:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
Time →
|
|
210
|
+
═══════════════════════════════════════════════════════════════════
|
|
211
|
+
chain1 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ (balance curve)
|
|
212
|
+
│ ──▶ T1 ──▶ T3 ←── R1 (rebalance from chain2)
|
|
213
|
+
───────┼───────────────────────────────────────────────────────────
|
|
214
|
+
chain2 │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
|
215
|
+
│ ──▶ T2 R1 ──▶
|
|
216
|
+
═══════════════════════════════════════════════════════════════════
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Features:
|
|
220
|
+
|
|
221
|
+
- **Transfer bars**: Horizontal bars showing transfer start → delivery (length = latency)
|
|
222
|
+
- **Rebalance markers**: Arrows showing rebalancer actions with direction
|
|
223
|
+
- **Balance curves**: Per-chain collateral over time
|
|
224
|
+
- **Hover tooltips**: Details on transfers, amounts, timing
|
|
225
|
+
- **KPI summary**: Completion rate, latencies, rebalance count
|
|
226
|
+
|
|
227
|
+
## Scenario Types
|
|
228
|
+
|
|
229
|
+
### Predefined Scenarios (in `scenarios/`)
|
|
230
|
+
|
|
231
|
+
| Scenario | Description | Expected Behavior |
|
|
232
|
+
| -------------------------------- | ----------------------------------- | ------------------------- |
|
|
233
|
+
| `extreme-drain-chain1` | 95% of transfers TO chain1 | Heavy rebalancing needed |
|
|
234
|
+
| `extreme-accumulate-chain1` | 95% of transfers FROM chain1 | Heavy rebalancing needed |
|
|
235
|
+
| `large-unidirectional-to-chain1` | 5 large (20 token) transfers | Immediate imbalance |
|
|
236
|
+
| `whale-transfers` | 3 massive (30 token) transfers | Stress test response time |
|
|
237
|
+
| `balanced-bidirectional` | Uniform random traffic | Minimal rebalancing |
|
|
238
|
+
| `surge-to-chain1` | Traffic spike mid-scenario | Tests burst handling |
|
|
239
|
+
| `stress-high-volume` | 50 transfers, Poisson distribution | Load testing |
|
|
240
|
+
| `moderate-imbalance-chain1` | 70% of transfers to chain1 | Moderate rebalancing |
|
|
241
|
+
| `sustained-drain-chain3` | 30 transfers over 30s | Endurance test |
|
|
242
|
+
| `random-with-headroom` | Random traffic with extra liquidity | Tests steady-state |
|
|
243
|
+
|
|
244
|
+
## Test Organization
|
|
245
|
+
|
|
246
|
+
### Unit Tests (`test/scenarios/`)
|
|
247
|
+
|
|
248
|
+
Test the scenario generation logic without running simulations:
|
|
249
|
+
|
|
250
|
+
- Does `unidirectionalFlow()` create correct transfer patterns?
|
|
251
|
+
- Does `randomTraffic()` distribute across all chains?
|
|
252
|
+
- Does serialization preserve BigInt amounts?
|
|
253
|
+
|
|
254
|
+
### Integration Tests (`test/integration/`)
|
|
255
|
+
|
|
256
|
+
Run full simulations on Anvil:
|
|
257
|
+
|
|
258
|
+
| Test File | Purpose |
|
|
259
|
+
| ------------------------- | ---------------------------------------------------- |
|
|
260
|
+
| `harness-setup.test.ts` | Verifies multi-domain deployment and harness setup |
|
|
261
|
+
| `full-simulation.test.ts` | Runs predefined scenarios, saves results |
|
|
262
|
+
| `inflight-guard.test.ts` | Demonstrates over-rebalancing without inflight guard |
|
|
263
|
+
|
|
264
|
+
### Why `inflight-guard.test.ts` is Separate
|
|
265
|
+
|
|
266
|
+
This test demonstrates a specific bug/limitation rather than testing a scenario type:
|
|
267
|
+
|
|
268
|
+
**What it proves:** Without tracking pending (inflight) transfers, the rebalancer sends redundant transfers because each poll sees "stale" on-chain balances.
|
|
269
|
+
|
|
270
|
+
**How it differs:**
|
|
271
|
+
|
|
272
|
+
- Uses custom inline scenario with extreme timing (3s bridge delay vs 200ms polling)
|
|
273
|
+
- Asserts on specific failure behavior (expects over-rebalancing)
|
|
274
|
+
- Documents a bug that needs fixing, not a passing scenario
|
|
275
|
+
|
|
276
|
+
## KPIs Collected
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
interface SimulationKPIs {
|
|
280
|
+
totalTransfers: number;
|
|
281
|
+
completedTransfers: number;
|
|
282
|
+
completionRate: number; // 0-1, should be >0.9 with working rebalancer
|
|
283
|
+
|
|
284
|
+
averageLatency: number; // ms
|
|
285
|
+
p50Latency: number;
|
|
286
|
+
p95Latency: number;
|
|
287
|
+
p99Latency: number;
|
|
288
|
+
|
|
289
|
+
totalRebalances: number;
|
|
290
|
+
rebalanceVolume: bigint; // Total tokens moved by rebalancer
|
|
291
|
+
|
|
292
|
+
perChainMetrics: Record<
|
|
293
|
+
string,
|
|
294
|
+
{
|
|
295
|
+
initialBalance: bigint;
|
|
296
|
+
finalBalance: bigint;
|
|
297
|
+
transfersIn: number;
|
|
298
|
+
transfersOut: number;
|
|
299
|
+
}
|
|
300
|
+
>;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Current Limitations
|
|
305
|
+
|
|
306
|
+
1. **No Inflight Guard**: Neither rebalancer implementation tracks pending transfers, causing over-rebalancing when bridge delays are long relative to polling frequency. The `inflight-guard.test.ts` demonstrates this.
|
|
307
|
+
|
|
308
|
+
2. **Single Anvil**: All "chains" run on one Anvil instance. Real cross-chain timing differences aren't simulated.
|
|
309
|
+
|
|
310
|
+
3. **Instant User Transfers**: User transfers via MockMailbox are instant. Real Hyperlane has ~15-30 second finality.
|
|
311
|
+
|
|
312
|
+
4. **No Gas Costs**: Gas costs aren't simulated. KPIs include rebalance count but not actual cost.
|
|
313
|
+
|
|
314
|
+
5. **Nonce Caching**: When running both rebalancers (`REBALANCERS=simple,production`), ethers v5 nonce caching can cause timeouts on the full test suite. Run specific scenarios for comparison.
|
|
315
|
+
|
|
316
|
+
6. **Production ActionTracker**: The `ProductionRebalancerRunner` uses a mock `ActionTracker` that does not persist state. The real production rebalancer's ActionTracker depends on external services not available in simulation. A mock ActionTracker with full in-memory tracking is planned for a future PR.
|
|
317
|
+
|
|
318
|
+
## Design Decisions
|
|
319
|
+
|
|
320
|
+
### Single Anvil, Multiple Domains
|
|
321
|
+
|
|
322
|
+
All simulated "chains" run on a single Anvil instance with different domain IDs:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// All chains share one RPC but have unique domain IDs
|
|
326
|
+
const chainMetadata = {
|
|
327
|
+
chain1: { domainId: 1000, rpcUrls: [{ http: anvilRpc }] },
|
|
328
|
+
chain2: { domainId: 2000, rpcUrls: [{ http: anvilRpc }] },
|
|
329
|
+
chain3: { domainId: 3000, rpcUrls: [{ http: anvilRpc }] },
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Each domain has its own:
|
|
333
|
+
// - MockMailbox (for instant user transfers)
|
|
334
|
+
// - HypERC20Collateral (warp token with liquidity)
|
|
335
|
+
// - MockValueTransferBridge (for delayed rebalancer transfers)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
This approach enables fast, deterministic testing without multi-process coordination.
|
|
339
|
+
|
|
340
|
+
### Fast Real-Time Execution
|
|
341
|
+
|
|
342
|
+
Simulations run in "compressed" real-time:
|
|
343
|
+
|
|
344
|
+
| Real World | Simulation Default |
|
|
345
|
+
| ------------- | ------------------ |
|
|
346
|
+
| 30s bridge | 500ms |
|
|
347
|
+
| 60s polling | 1000ms |
|
|
348
|
+
| 5min scenario | ~10s |
|
|
349
|
+
|
|
350
|
+
Configure via `SimulationTiming`:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
interface SimulationTiming {
|
|
354
|
+
bridgeDeliveryDelay: number; // ms - bridge transfer time
|
|
355
|
+
rebalancerPollingFrequency: number; // ms - how often rebalancer checks
|
|
356
|
+
userTransferInterval: number; // ms - spacing between user transfers
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Observation Isolation
|
|
361
|
+
|
|
362
|
+
Rebalancers can ONLY observe state via:
|
|
363
|
+
|
|
364
|
+
- JSON-RPC balance queries (`eth_call` to ERC20.balanceOf)
|
|
365
|
+
- Event logs (`eth_getLogs`)
|
|
366
|
+
- View functions (ISM queries, router configs)
|
|
367
|
+
|
|
368
|
+
NOT allowed:
|
|
369
|
+
|
|
370
|
+
- Direct contract object access
|
|
371
|
+
- Simulation internal state
|
|
372
|
+
- Bridge controller pending queue
|
|
373
|
+
|
|
374
|
+
This ensures the simulation tests realistic rebalancer behavior.
|
|
375
|
+
|
|
376
|
+
## Programmatic Usage
|
|
377
|
+
|
|
378
|
+
### Basic Simulation
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
import {
|
|
382
|
+
RebalancerSimulationHarness,
|
|
383
|
+
ScenarioLoader,
|
|
384
|
+
SimpleRunner,
|
|
385
|
+
} from '@hyperlane-xyz/rebalancer-sim';
|
|
386
|
+
|
|
387
|
+
// Load scenario from JSON
|
|
388
|
+
const scenario = ScenarioLoader.loadScenario('balanced-bidirectional');
|
|
389
|
+
|
|
390
|
+
// Create and initialize harness (deploys contracts on anvil)
|
|
391
|
+
const harness = new RebalancerSimulationHarness({
|
|
392
|
+
anvilRpc: 'http://localhost:8545',
|
|
393
|
+
initialCollateralBalance: BigInt(scenario.defaultInitialCollateral),
|
|
394
|
+
});
|
|
395
|
+
await harness.initialize();
|
|
396
|
+
|
|
397
|
+
// Run simulation
|
|
398
|
+
const result = await harness.runSimulation(scenario, new SimpleRunner(), {
|
|
399
|
+
bridgeConfig: scenario.defaultBridgeConfig,
|
|
400
|
+
timing: scenario.defaultTiming,
|
|
401
|
+
strategyConfig: scenario.defaultStrategyConfig,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
console.log(`Completion: ${result.kpis.completionRate * 100}%`);
|
|
405
|
+
console.log(`Avg Latency: ${result.kpis.averageLatency}ms`);
|
|
406
|
+
console.log(`Rebalances: ${result.kpis.totalRebalances}`);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Compare Rebalancers
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import {
|
|
413
|
+
ProductionRebalancerRunner,
|
|
414
|
+
SimpleRunner,
|
|
415
|
+
} from '@hyperlane-xyz/rebalancer-sim';
|
|
416
|
+
|
|
417
|
+
const rebalancers = [
|
|
418
|
+
new SimpleRunner(), // Simplified baseline
|
|
419
|
+
new ProductionRebalancerRunner(), // Production rebalancer service
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
// compareRebalancers() handles state reset internally
|
|
423
|
+
const report = await harness.compareRebalancers(scenario, rebalancers, {
|
|
424
|
+
strategyConfig: scenario.defaultStrategyConfig,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
for (const result of report.results) {
|
|
428
|
+
console.log(`${result.rebalancerName}: ${result.kpis.completionRate * 100}%`);
|
|
429
|
+
}
|
|
430
|
+
console.log(`Best latency: ${report.comparison.bestLatency}`);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Generate Custom Scenarios
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { parseEther } from 'ethers/lib/utils';
|
|
437
|
+
|
|
438
|
+
import { ScenarioGenerator } from '@hyperlane-xyz/rebalancer-sim';
|
|
439
|
+
|
|
440
|
+
// Unidirectional flow (tests drain)
|
|
441
|
+
const drainScenario = ScenarioGenerator.unidirectionalFlow({
|
|
442
|
+
origin: 'chain1',
|
|
443
|
+
destination: 'chain2',
|
|
444
|
+
transferCount: 100,
|
|
445
|
+
duration: 10000,
|
|
446
|
+
amount: parseEther('1'),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Random traffic across all chains
|
|
450
|
+
const randomScenario = ScenarioGenerator.randomTraffic({
|
|
451
|
+
chains: ['chain1', 'chain2', 'chain3'],
|
|
452
|
+
transferCount: 50,
|
|
453
|
+
duration: 5000,
|
|
454
|
+
amountRange: [parseEther('1'), parseEther('10')],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Surge pattern (spike mid-scenario)
|
|
458
|
+
const surgeScenario = ScenarioGenerator.surgeScenario({
|
|
459
|
+
chains: ['chain1', 'chain2', 'chain3'],
|
|
460
|
+
baselineRate: 1, // 1 tx/s baseline
|
|
461
|
+
surgeMultiplier: 5, // 5x during surge
|
|
462
|
+
surgeStart: 3000,
|
|
463
|
+
surgeDuration: 2000,
|
|
464
|
+
totalDuration: 10000,
|
|
465
|
+
amountRange: [parseEther('1'), parseEther('5')],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Balanced bidirectional traffic (equal in/out per chain)
|
|
469
|
+
const balancedScenario = ScenarioGenerator.balancedTraffic({
|
|
470
|
+
chains: ['chain1', 'chain2', 'chain3'],
|
|
471
|
+
pairCount: 10, // Creates 20 transfers (10 pairs of A→B, B→A)
|
|
472
|
+
duration: 5000,
|
|
473
|
+
amountRange: [parseEther('1'), parseEther('5')],
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Future Work
|
|
478
|
+
|
|
479
|
+
### Phase 9: Backtesting with Real Warp Route History
|
|
480
|
+
|
|
481
|
+
**Goal:** Replay historical warp route traffic to backtest rebalancer strategies.
|
|
482
|
+
|
|
483
|
+
**Planned implementation:**
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// Load historical transfers from explorer or indexer
|
|
487
|
+
const historicalTransfers = await fetchWarpRouteHistory({
|
|
488
|
+
warpRouteId: 'ETH/USDC-ethereum-arbitrum-optimism',
|
|
489
|
+
startDate: '2024-01-01',
|
|
490
|
+
endDate: '2024-03-01',
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Convert to scenario format
|
|
494
|
+
const scenario = ScenarioGenerator.fromHistoricalData(historicalTransfers);
|
|
495
|
+
|
|
496
|
+
// Run simulation with historical traffic
|
|
497
|
+
const result = await harness.runSimulation(scenario, rebalancer, config);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Benefits:**
|
|
501
|
+
|
|
502
|
+
- Validate strategies against real-world traffic patterns
|
|
503
|
+
- Identify edge cases that synthetic scenarios miss
|
|
504
|
+
- Compare how different strategies would have performed historically
|
|
505
|
+
|
|
506
|
+
### Phase 10: Mock Explorer API for Inflight Guard
|
|
507
|
+
|
|
508
|
+
**Goal:** Enable testing of inflight guard functionality without real Explorer infrastructure.
|
|
509
|
+
|
|
510
|
+
The real rebalancer uses `WithInflightGuard` wrapper that queries Hyperlane Explorer API to track pending (inflight) transfers. This prevents over-rebalancing by accounting for transfers in the bridge pipeline.
|
|
511
|
+
|
|
512
|
+
**Planned implementation:**
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// src/mocks/MockExplorerApi.ts
|
|
516
|
+
export class MockExplorerApi {
|
|
517
|
+
// Called by BridgeMockController when transfer initiated
|
|
518
|
+
registerPendingTransfer(transfer: {
|
|
519
|
+
messageId;
|
|
520
|
+
origin;
|
|
521
|
+
destination;
|
|
522
|
+
amount;
|
|
523
|
+
}): void;
|
|
524
|
+
|
|
525
|
+
// Called by BridgeMockController when transfer delivered
|
|
526
|
+
markDelivered(messageId: string): void;
|
|
527
|
+
|
|
528
|
+
// Called by rebalancer's inflight guard
|
|
529
|
+
async getInflightTransfers(origin, destination): Promise<InflightTransfer[]>;
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Integration points:**
|
|
534
|
+
|
|
535
|
+
- BridgeMockController calls `registerPendingTransfer()` on bridge initiation
|
|
536
|
+
- BridgeMockController calls `markDelivered()` on bridge delivery
|
|
537
|
+
- RealRebalancerRunner injects mock API for inflight queries
|
|
538
|
+
|
|
539
|
+
**Expected outcome:** `inflight-guard.test.ts` should PASS (1-2 rebalances instead of 30+) once mock explorer is integrated.
|
|
540
|
+
|
|
541
|
+
### Phase 11: Advanced Scenarios
|
|
542
|
+
|
|
543
|
+
**Bridge Failures and Latency Variance**
|
|
544
|
+
|
|
545
|
+
- Configure `failureRate > 0` in bridge config
|
|
546
|
+
- Test rebalancer recovery after partial failures
|
|
547
|
+
- Verify no stuck state after transient failures
|
|
548
|
+
- Asymmetric delays: `chain1→chain2: 500ms`, `chain2→chain1: 2000ms`
|
|
549
|
+
- Variable latency per route for heterogeneous bridge environments
|
|
550
|
+
|
|
551
|
+
**Rebalancer Restart**
|
|
552
|
+
|
|
553
|
+
- Stop rebalancer mid-scenario, restart
|
|
554
|
+
- Verify recovery and correct state resumption
|
|
555
|
+
- Test idempotency of rebalance operations
|
|
556
|
+
|
|
557
|
+
**Scoring Based on Rebalancing Cost**
|
|
558
|
+
|
|
559
|
+
- Mock gas prices per chain
|
|
560
|
+
- Track total gas cost in KPIs (already partially implemented)
|
|
561
|
+
- Add rebalancing cost as scoring metric:
|
|
562
|
+
```typescript
|
|
563
|
+
const score =
|
|
564
|
+
completionRate * 0.5 +
|
|
565
|
+
(1 - normalizedLatency) * 0.3 +
|
|
566
|
+
(1 - normalizedCost) * 0.2;
|
|
567
|
+
```
|
|
568
|
+
- Compare strategies by cost-efficiency ratio
|
|
569
|
+
|
|
570
|
+
### Phase 12: Enhanced Visualization
|
|
571
|
+
|
|
572
|
+
**Real-time dashboard** (stretch goal):
|
|
573
|
+
|
|
574
|
+
- WebSocket updates during simulation
|
|
575
|
+
- Live balance curves
|
|
576
|
+
- Transfer animation
|
|
577
|
+
|
|
578
|
+
**Comparison views:**
|
|
579
|
+
|
|
580
|
+
- Side-by-side rebalancer comparison in single HTML
|
|
581
|
+
- Diff highlighting for KPI differences
|
|
582
|
+
- Strategy effectiveness scoring
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import type { BridgeMockConfig, DeployedDomain, PendingTransfer } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* BridgeMockController manages simulated bridge transfers with configurable
|
|
6
|
+
* delays, failures, and fees. It intercepts SentTransferRemote events and
|
|
7
|
+
* schedules async delivery to simulate real bridge behavior.
|
|
8
|
+
*/
|
|
9
|
+
export declare class BridgeMockController extends EventEmitter {
|
|
10
|
+
private readonly provider;
|
|
11
|
+
private readonly domains;
|
|
12
|
+
private readonly deployerKey;
|
|
13
|
+
private readonly bridgeConfig;
|
|
14
|
+
private pendingTransfers;
|
|
15
|
+
private completedTransfers;
|
|
16
|
+
private transferCounter;
|
|
17
|
+
private deliveryTimers;
|
|
18
|
+
private isRunning;
|
|
19
|
+
private eventListeners;
|
|
20
|
+
private txQueue;
|
|
21
|
+
private txProcessing;
|
|
22
|
+
constructor(provider: ethers.providers.JsonRpcProvider, domains: Record<string, DeployedDomain>, deployerKey: string, bridgeConfig?: BridgeMockConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Queue a transaction to be executed serially (prevents nonce collisions)
|
|
25
|
+
*/
|
|
26
|
+
private queueTransaction;
|
|
27
|
+
/**
|
|
28
|
+
* Process queued transactions one at a time
|
|
29
|
+
*/
|
|
30
|
+
private processQueue;
|
|
31
|
+
/**
|
|
32
|
+
* Gets the bridge config for a specific route
|
|
33
|
+
*/
|
|
34
|
+
private getRouteConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Calculates delivery delay with jitter
|
|
37
|
+
*/
|
|
38
|
+
private calculateDelay;
|
|
39
|
+
/**
|
|
40
|
+
* Start listening for bridge events
|
|
41
|
+
*/
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Stop listening and cancel pending deliveries
|
|
45
|
+
*/
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Handle transfer initiated event
|
|
49
|
+
*/
|
|
50
|
+
private onTransferInitiated;
|
|
51
|
+
/**
|
|
52
|
+
* Execute delivery of a pending transfer
|
|
53
|
+
*/
|
|
54
|
+
private executeDelivery;
|
|
55
|
+
/**
|
|
56
|
+
* Simulate bridge delivery by burning from origin bridge and minting to destination.
|
|
57
|
+
* This maintains token conservation across the simulation.
|
|
58
|
+
* Uses transaction queue to prevent nonce collisions.
|
|
59
|
+
*/
|
|
60
|
+
private simulateBridgeDelivery;
|
|
61
|
+
/**
|
|
62
|
+
* Manually trigger delivery for a pending transfer (for testing)
|
|
63
|
+
*/
|
|
64
|
+
forceDelivery(transferId: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Check if there are pending transfers
|
|
67
|
+
*/
|
|
68
|
+
hasPendingTransfers(): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Get count of pending transfers
|
|
71
|
+
*/
|
|
72
|
+
getPendingCount(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Get all pending transfers
|
|
75
|
+
*/
|
|
76
|
+
getPendingTransfers(): PendingTransfer[];
|
|
77
|
+
/**
|
|
78
|
+
* Get completed transfers
|
|
79
|
+
*/
|
|
80
|
+
getCompletedTransfers(): PendingTransfer[];
|
|
81
|
+
/**
|
|
82
|
+
* Wait for all pending transfers to complete
|
|
83
|
+
* On timeout, marks remaining transfers as failed and clears them
|
|
84
|
+
*/
|
|
85
|
+
waitForAllDeliveries(timeoutMs?: number): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=BridgeMockController.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BridgeMockController.d.ts","sourceRoot":"","sources":["../src/BridgeMockController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAStC,OAAO,KAAK,EAEV,gBAAgB,EAEhB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAKpB;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;IAalD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAf/B,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAA2C;IAGjE,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,YAAY,CAAS;gBAGV,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,eAAe,EAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EACvC,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,gBAAqB;IAKtD;;OAEG;YACW,gBAAgB;IAc9B;;OAEG;YACW,YAAY;IAiB1B;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,OAAO,CAAC,cAAc;IAKtB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B;;OAEG;YACW,mBAAmB;IAiEjC;;OAEG;YACW,eAAe;IAwD7B;;;;OAIG;YACW,sBAAsB;IAgCpC;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtD;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAIxC;;OAEG;IACH,qBAAqB,IAAI,eAAe,EAAE;IAI1C;;;OAGG;IACG,oBAAoB,CAAC,SAAS,GAAE,MAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CA2BrE"}
|