Skip to content

Commit 5bf1966

Browse files
authored
Merge pull request #136 from synonymdev/feat/settings-v60
Feat/settings v60
2 parents e703f3c + 130d258 commit 5bf1966

18 files changed

Lines changed: 252 additions & 226 deletions

docs/lightning-primer-for-qa.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ The **fix path** uses **`accept_stale_channel_monitors`** so ldk-node can align
3434

3535
## What to test when Lightning / LDK storage changes
3636

37-
| Area | Why |
38-
|------|-----|
39-
| **Cold start** | Any path that reads/writes ChannelManager, monitors, or VSS must not pair **new** manager with **old** monitor. |
40-
| **Backup / restore** | Restoring must be **consistent snapshots**; partial or older monitor alone is high risk. |
41-
| **Migration** | RN → native or schema changes: avoid overwriting live data with **stale** remote copies. |
42-
| **Recovery** | After `DangerousValue` / `accept_stale`: peers reconnect, chain sync completes, **inbound and outbound** payments work, **second launch** does not repeat recovery forever. |
43-
| **Infra noise** | On regtest, **stale RGS** / gossip can cause transient **“route not found”** — distinguish from persistence bugs (see logs for `DangerousValue` vs routing errors). |
37+
| Area | Why |
38+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39+
| **Cold start** | Any path that reads/writes ChannelManager, monitors, or VSS must not pair **new** manager with **old** monitor. |
40+
| **Backup / restore** | Restoring must be **consistent snapshots**; partial or older monitor alone is high risk. |
41+
| **Migration** | RN → native or schema changes: avoid overwriting live data with **stale** remote copies. |
42+
| **Recovery** | After `DangerousValue` / `accept_stale`: peers reconnect, chain sync completes, **inbound and outbound** payments work, **second launch** does not repeat recovery forever. |
43+
| **Infra noise** | On regtest, **stale RGS** / gossip can cause transient **“route not found”** — distinguish from persistence bugs (see logs for `DangerousValue` vs routing errors). |
4444

4545
## Risks of incorrect “fixes”
4646

@@ -50,13 +50,13 @@ The **fix path** uses **`accept_stale_channel_monitors`** so ldk-node can align
5050

5151
## Glossary
5252

53-
| Term | Meaning |
54-
|------|--------|
55-
| **Commitment update** | New off-chain state (balances + HTLC set). |
56-
| **`update_id`** | LDK’s persisted notion of how far the ChannelMonitor has advanced vs the ChannelManager for that channel. |
57-
| **HTLC** | **Hash Time-Locked Contract** — conditional payment inside a commitment (hash lock + time lock). |
58-
| **ChannelMonitor** | Per-channel persisted state for chain watching and dispute handling. |
59-
| **DangerousValue** | LDK/ldk-node refusing to load because continuing would violate safety assumptions (e.g. stale monitor). |
53+
| Term | Meaning |
54+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
55+
| **Commitment update** | New off-chain state (balances + HTLC set). |
56+
| **`update_id`** | LDK’s persisted notion of how far the ChannelMonitor has advanced vs the ChannelManager for that channel. |
57+
| **HTLC** | **Hash Time-Locked Contract** — conditional payment inside a commitment (hash lock + time lock). |
58+
| **ChannelMonitor** | Per-channel persisted state for chain watching and dispute handling. |
59+
| **DangerousValue** | LDK/ldk-node refusing to load because continuing would violate safety assumptions (e.g. stale monitor). |
6060
| **accept_stale_channel_monitors** | Explicit recovery mode to load despite mismatch, then heal via protocol + sync (use only in controlled recovery). |
6161

6262
## See also

docs/repro-channel-monitor-desync.md

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
See also: [Lightning primer for QA](./lightning-primer-for-qa.md) (monitors, HTLCs, gaps, risks).
44

55
Related issues:
6+
67
- [#847 (bitkit-android)](https://github.com/synonymdev/bitkit-android/issues/847)
78
- iOS support ticket (user logs from 2026-03-18)
89

910
Fix branches:
11+
1012
- **iOS**: `fix/stale-monitor-recovery-release`
1113
- **Android**: `fix/stale-monitor-recovery-v2`
1214

@@ -17,6 +19,7 @@ Build 182 (v2.1.0) introduced `fetchOrphanedChannelMonitorsIfNeeded` which fetch
1719
## Root Cause
1820

1921
On v2.1.0 startup:
22+
2023
1. `fetchOrphanedChannelMonitorsIfNeeded` fetches stale channel monitor from RN backup server
2124
2. Injects it via `setChannelDataMigration` with `channelManager: nil` (monitors only)
2225
3. ldk-node persists the stale monitor to VSS/local storage
@@ -33,6 +36,7 @@ Failed to read channel manager from store: Value would be dangerous to continue
3336
```
3437

3538
In app logs:
39+
3640
```
3741
Running pre-startup channel monitor recovery check
3842
Found 1 monitors on RN backup for pre-startup recovery
@@ -89,6 +93,7 @@ RN v1.1.6 local builds use `.env.test.template` (regtest + localhost Electrum).
8993
**Critical**: The RN app's `.env.production` must point the backup server to **staging** (not localhost), because the native apps have `rnBackupServerHost` hardcoded to staging. If the RN app pushes to `127.0.0.1:3003` but the native app queries `bitkit.stag0.blocktank.to`, it will never find the channel monitors and the bug won't trigger.
9094

9195
In `.env.production` for the RN v1.1.6 build, set:
96+
9297
```
9398
BACKUPS_SERVER_HOST=https://bitkit.stag0.blocktank.to/backups-ldk
9499
BACKUPS_SERVER_PUBKEY=02c03b8b8c1b5500b622646867d99bf91676fac0f38e2182c91a9ff0d053a21d6d
@@ -130,6 +135,7 @@ The app's channel has all balance on the app side. LND needs outbound liquidity
130135
```
131136

132137
Verify with a test payment:
138+
133139
```bash
134140
./bitcoin-cli payinvoice <bolt11_invoice> 10
135141
```
@@ -166,6 +172,7 @@ Install v2.1.0 **over** the native app → app fails to start LN node (see error
166172
Upgrading from a broken v2.1.0 wallet to v2.1.2 (fix candidate) recovers the wallet. Channels are healed and LN transactions work after recovery.
167173

168174
Fix branches:
175+
169176
- **iOS**: `fix/stale-monitor-recovery-release`
170177
- **Android**: `fix/stale-monitor-recovery-v2`
171178

@@ -192,34 +199,34 @@ Matrix of upgrade/recovery scenarios to validate v2.1.2. Each scenario should be
192199

193200
### Blocktank channel (staging regtest)
194201

195-
| # | Scenario | Result |
196-
|---|----------|--------|
197-
| B1 | v2.0.6 (wallet with 21+ payment gap) → v2.1.0 → confirm broken | Reproduces |
198-
| B2 | Restore broken v2.1.0 wallet into v2.1.2 (clean install + restore) | ✅ Recovered |
199-
| B3 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered |
200-
| B4 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues |
201-
| B5 | v2.0.6 (wallet with gap) → v2.1.1 → v2.1.2 | ✅ No issues |
202-
| B6 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues |
203-
| B7 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered |
202+
| # | Scenario | Result |
203+
| --- | -------------------------------------------------------------------- | ------------ |
204+
| B1 | v2.0.6 (wallet with 21+ payment gap) → v2.1.0 → confirm broken | Reproduces |
205+
| B2 | Restore broken v2.1.0 wallet into v2.1.2 (clean install + restore) | ✅ Recovered |
206+
| B3 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered |
207+
| B4 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues |
208+
| B5 | v2.0.6 (wallet with gap) → v2.1.1 → v2.1.2 | ✅ No issues |
209+
| B6 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues |
210+
| B7 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered |
204211

205212
### 3rd-party channel (local docker)
206213

207-
| # | Scenario | Result |
208-
|---|----------|--------|
209-
| T1 | v2.0.6 (wallet with 30+ payment gap) → v2.1.0 → confirm broken | Reproduces |
210-
| T2 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered |
211-
| T3 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues |
212-
| T4 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues |
213-
| T5 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered |
214+
| # | Scenario | Result |
215+
| --- | -------------------------------------------------------------------- | ------------ |
216+
| T1 | v2.0.6 (wallet with 30+ payment gap) → v2.1.0 → confirm broken | Reproduces |
217+
| T2 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered |
218+
| T3 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues |
219+
| T4 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues |
220+
| T5 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered |
214221

215222
### Version reference
216223

217-
| Version | iOS branch | Android branch |
218-
|---------|-----------|---------------|
219-
| v1.1.6 | tag `v1.1.6` (RN) | tag `v1.1.6` (RN) |
220-
| v2.0.6 | `chore/e2e-updater-url` ||
221-
| v2.0.3 || `chore/e2e-updater-url` |
222-
| v2.1.0 | build 182 | build 182 |
224+
| Version | iOS branch | Android branch |
225+
| ------------ | ------------------------------------ | ------------------------------- |
226+
| v1.1.6 | tag `v1.1.6` (RN) | tag `v1.1.6` (RN) |
227+
| v2.0.6 | `chore/e2e-updater-url` | |
228+
| v2.0.3 | | `chore/e2e-updater-url` |
229+
| v2.1.0 | build 182 | build 182 |
223230
| v2.1.2 (fix) | `fix/stale-monitor-recovery-release` | `fix/stale-monitor-recovery-v2` |
224231

225232
---
@@ -233,11 +240,11 @@ Matrix of upgrade/recovery scenarios to validate v2.1.2. Each scenario should be
233240

234241
## Files
235242

236-
| File | Purpose |
237-
|------|---------|
238-
| `docs/lightning-primer-for-qa.md` | Background: ChannelManager vs ChannelMonitor, HTLCs, gaps, test focus |
239-
| `test/specs/receive-ln-payments.e2e.ts` | Automated spec to receive N Lightning payments |
240-
| `wdio.no-install.conf.ts` | WDIO config that attaches to existing app (no reinstall) |
241-
| `docker/bitcoin-cli` | Local docker CLI with `openchannel`, `payinvoice`, `mine`, `send` commands |
242-
| `scripts/pay-lightning-address.sh` | Shell script to pay BOLT11/BIP21/LN address via Blocktank |
243-
| `scripts/pay-lightning-address-loop.sh` | Shell script to send N payments to a Lightning address |
243+
| File | Purpose |
244+
| --------------------------------------- | -------------------------------------------------------------------------- |
245+
| `docs/lightning-primer-for-qa.md` | Background: ChannelManager vs ChannelMonitor, HTLCs, gaps, test focus |
246+
| `test/specs/receive-ln-payments.e2e.ts` | Automated spec to receive N Lightning payments |
247+
| `wdio.no-install.conf.ts` | WDIO config that attaches to existing app (no reinstall) |
248+
| `docker/bitcoin-cli` | Local docker CLI with `openchannel`, `payinvoice`, `mine`, `send` commands |
249+
| `scripts/pay-lightning-address.sh` | Shell script to pay BOLT11/BIP21/LN address via Blocktank |
250+
| `scripts/pay-lightning-address-loop.sh` | Shell script to send N payments to a Lightning address |

test/helpers/actions.ts

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { ChainablePromiseElement } from 'webdriverio';
22
import { reinstallApp } from './setup';
33
import { deposit, mineBlocks } from './regtest';
4+
import { doNavigationClose, doTriggerTimedSheet, openSettings } from './navigation';
5+
6+
export { doNavigationClose, doTriggerTimedSheet } from './navigation';
47

58
export const sleep = (ms: number) => browser.pause(ms);
69

@@ -603,16 +606,8 @@ export async function handleAndroidAlert(
603606
}
604607
}
605608

606-
export async function doNavigationClose() {
607-
await tap('HeaderMenu');
608-
await tap('DrawerWallet');
609-
await sleep(500);
610-
}
611-
612609
export async function getSeed(): Promise<string> {
613-
await tap('HeaderMenu');
614-
await tap('DrawerSettings');
615-
await tap('BackupSettings');
610+
await openSettings('security');
616611
await tap('BackupWallet');
617612

618613
// get seed from SeedContainer
@@ -810,9 +805,7 @@ async function assertAddressTypeSwitchFeedback() {
810805
}
811806

812807
export async function switchPrimaryAddressType(nextType: addressTypePreference) {
813-
await tap('HeaderMenu');
814-
await tap('DrawerSettings');
815-
await tap('AdvancedSettings');
808+
await openSettings('advanced');
816809
await tap('AddressTypePreference');
817810
await tap(nextType);
818811
await assertAddressTypeSwitchFeedback();
@@ -1165,6 +1158,8 @@ export type ToastId =
11651158
| 'TransactionRemovedToast'
11661159
| 'InvalidAddressToast'
11671160
| 'ExpiredLightningToast'
1161+
| 'DevModeEnabledToast'
1162+
| 'DevModeDisabledToast'
11681163
| 'InsufficientSpendingToast'
11691164
| 'InsufficientSavingsToast';
11701165

@@ -1204,23 +1199,6 @@ export async function acknowledgeExternalSuccess() {
12041199
await sleep(300);
12051200
}
12061201

1207-
/**
1208-
* Triggers the timed backup sheet by navigating to settings and back.
1209-
* Since timed sheets are sometimes triggered by user behavior (when user goes back to home screen),
1210-
* we need to trigger them manually.
1211-
*
1212-
* @example
1213-
* // Trigger backup sheet before testing dismissal
1214-
* await doTriggerTimedSheet();
1215-
*/
1216-
export async function doTriggerTimedSheet() {
1217-
await sleep(700); // wait for any previous animations to finish
1218-
await tap('HeaderMenu');
1219-
await tap('DrawerSettings');
1220-
await sleep(500); // wait for the app to settle
1221-
await doNavigationClose();
1222-
}
1223-
12241202
export async function dismissBackgroundPaymentsTimedSheet({
12251203
triggerTimedSheet = false,
12261204
}: { triggerTimedSheet?: boolean } = {}) {
@@ -1363,9 +1341,7 @@ export async function acknowledgeHighBalanceWarning({
13631341
// enable/disable widgets in settings
13641342
export async function toggleWidgets() {
13651343
await sleep(3000);
1366-
await tap('HeaderMenu');
1367-
await tap('DrawerSettings');
1368-
await tap('GeneralSettings');
1344+
await openSettings();
13691345
await tap('WidgetsSettings');
13701346
const widgets = await elementsByText('Widgets');
13711347
await widgets[1].click();
@@ -1476,8 +1452,7 @@ export async function attemptRefreshOnHomeScreen() {
14761452
}
14771453

14781454
export async function waitForBackup() {
1479-
await tap('HeaderMenu');
1480-
await tap('DrawerSettings');
1455+
await openSettings('security');
14811456
await tap('BackupSettings');
14821457
await elementById('AllSynced').waitForDisplayed();
14831458
await doNavigationClose();

test/helpers/lnd.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
typeText,
1111
} from './actions';
1212
import { LndConfig } from './constants';
13+
import { openSettings } from './navigation';
1314
import createLndRpc, { LnRpc, WalletUnlockerRpc } from '@radar/lnrpc';
1415

1516
export async function setupLND(
@@ -94,9 +95,7 @@ export async function waitForActiveChannel(
9495
}
9596

9697
export async function getLDKNodeID(): Promise<string> {
97-
await tap('HeaderMenu');
98-
await tap('DrawerSettings');
99-
await tap('AdvancedSettings');
98+
await openSettings('advanced');
10099
// wait for LDK to start
101100
await sleep(5000);
102101
await tap('LightningNodeInfo');
@@ -123,9 +122,7 @@ export async function connectToLND(lndNodeID: string, { navigationClose = true }
123122
}
124123

125124
export async function checkChannelStatus({ size = '100 000' } = {}) {
126-
await tap('HeaderMenu');
127-
await tap('DrawerSettings');
128-
await tap('AdvancedSettings');
125+
await openSettings('advanced');
129126
await tap('Channels');
130127
await sleep(1000);
131128
await tap('Channel');

test/helpers/navigation.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { tap, sleep } from './actions';
2+
3+
export type SettingsTab = 'general' | 'security' | 'advanced';
4+
5+
/**
6+
* Opens the Settings screen at the given tab.
7+
* General is the default tab so no extra tap is needed for it.
8+
*/
9+
export async function openSettings(tab: SettingsTab = 'general') {
10+
await tap('HeaderMenu');
11+
await tap('DrawerSettings');
12+
if (tab !== 'general') {
13+
await tap(`Tab-${tab}`);
14+
await sleep(300);
15+
}
16+
}
17+
18+
/**
19+
* Opens the Support screen from the drawer menu.
20+
*/
21+
export async function openSupport() {
22+
await tap('HeaderMenu');
23+
await tap('DrawerSupport');
24+
}
25+
26+
/**
27+
* Closes the drawer and navigates back to the Wallet home screen.
28+
*/
29+
export async function doNavigationClose() {
30+
await tap('HeaderMenu');
31+
await tap('DrawerWallet');
32+
await sleep(500);
33+
}
34+
35+
/**
36+
* Triggers the timed backup sheet by navigating to settings and back.
37+
* Since timed sheets are sometimes triggered by user behavior (when user goes back to home screen),
38+
* we need to trigger them manually.
39+
*/
40+
export async function doTriggerTimedSheet() {
41+
await sleep(700);
42+
await tap('HeaderMenu');
43+
await tap('DrawerSettings');
44+
await sleep(500);
45+
await doNavigationClose();
46+
}

test/helpers/regtest.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async function localMineBlocks(count: number): Promise<void> {
5151

5252
function lndRestRequest(
5353
path: string,
54-
body: Record<string, unknown>,
54+
body: Record<string, unknown>
5555
): Promise<Record<string, unknown>> {
5656
const tlsCert = fs.readFileSync(lndConfig.tls);
5757
const macaroon = fs.readFileSync(lndConfig.macaroonPath).toString('hex');
@@ -74,15 +74,17 @@ function lndRestRequest(
7474
},
7575
(res) => {
7676
let data = '';
77-
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
77+
res.on('data', (chunk: Buffer) => {
78+
data += chunk.toString();
79+
});
7880
res.on('end', () => {
7981
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
8082
resolve(JSON.parse(data) as Record<string, unknown>);
8183
} else {
8284
reject(new Error(`LND REST ${res.statusCode}: ${data}`));
8385
}
8486
});
85-
},
87+
}
8688
);
8789
req.on('error', reject);
8890
req.write(payload);

test/specs/backup.e2e.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '../helpers/actions';
2020
import { ciIt } from '../helpers/suite';
2121
import { ensureLocalFunds } from '../helpers/regtest';
22+
import { openSettings } from '../helpers/navigation';
2223

2324
describe('@backup - Backup', () => {
2425
let electrum: Awaited<ReturnType<typeof initElectrum>> | undefined;
@@ -66,9 +67,7 @@ describe('@backup - Backup', () => {
6667
await tap('NavigationBack');
6768

6869
// - change settings (currency to GBP) //
69-
await tap('HeaderMenu');
70-
await tap('DrawerSettings');
71-
await tap('GeneralSettings');
70+
await openSettings();
7271
await tap('CurrenciesSettings');
7372
const gbp_opt = await elementByText('GBP (£)');
7473
await gbp_opt.waitForDisplayed();

0 commit comments

Comments
 (0)