Skip to content

Commit bafd8f5

Browse files
committed
recovery: add backup and restore package
Introduce a dedicated recovery package for encrypted local backups of static-address state and raw l402 token files. The package owns the backup file format, seed-derived encryption, static-address key re-derivation with gap fallback, token file restore, and best-effort deposit reconciliation orchestration. It also includes package-level documentation and focused tests for the file helpers.
1 parent f986b14 commit bafd8f5

6 files changed

Lines changed: 2797 additions & 6 deletions

File tree

recovery/README.md

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Recovery Package
2+
3+
This package implements local recovery for Loop's static-address and L402
4+
state.
5+
6+
## Goal
7+
8+
Recovery is generation-based. A generation is anchored by:
9+
10+
- one paid L402 token
11+
- one immutable static-address generation root tied to that L402
12+
13+
Today that generation root still materializes locally as one legacy static
14+
address. The backup format now also stores the stable generation metadata that
15+
future multi-address `main`/`change` issuance will recover by scanning from,
16+
without rewriting the backup file.
17+
18+
The recovery flow is designed to let a fresh or repaired Loop instance rebuild
19+
that generation after local disk loss, data-directory replacement, or partial
20+
corruption.
21+
22+
The current PR intentionally uses a single immutable backup per L402
23+
generation. Once written, that backup file is never updated in place.
24+
25+
## Backup Model
26+
27+
The daemon writes at most one encrypted backup file for each paid L402 token
28+
ID:
29+
30+
`<loop-data-dir>/L402_backup_<l402-created-at-unix-ns>_<l402-token-id>.enc`
31+
32+
In the normal layout this resolves inside the active network-specific Loop data
33+
directory, for example:
34+
35+
`~/.loop/mainnet/L402_backup_1776159001000000000_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef.enc`
36+
37+
There is no canonical mutable "latest backup" file anymore.
38+
39+
If `loop recover` is called without `--backup_file`, Loop scans the active
40+
network directory, validates all immutable backups it can decrypt, and restores
41+
the backup with the latest timestamp in its filename. Legacy timestamp-less
42+
backup filenames remain readable and fall back to the encrypted payload's L402
43+
creation time for ordering.
44+
45+
## What Is Backed Up
46+
47+
Each encrypted backup stores:
48+
49+
- a backup format version
50+
- the Loop network
51+
- the paid L402 token ID
52+
- the paid L402 token creation time
53+
- the raw paid `l402.token` file
54+
- immutable static-address generation metadata
55+
- the static-address protocol version
56+
- the generation server pubkey
57+
- the static-address expiry
58+
- the `main` key family
59+
- the `change` key locator
60+
- the `change` key family
61+
- the `change` key index
62+
- the generation start height
63+
- the restore lookahead
64+
- the current legacy static-address materialization
65+
- the static-address client pubkey
66+
- the static-address client key locator
67+
- the static-address `pkScript`
68+
- the derived taproot address string
69+
- the static-address initiation height
70+
71+
The L402 file is preserved as a raw blob so restore remains compatible with the
72+
current Aperture token-store format.
73+
74+
Deposit FSM state is not serialized into the backup. Deposits are rediscovered
75+
after restore through the normal reconciliation path.
76+
77+
## Why The Backup Stores Both Root And Legacy Address Data
78+
79+
The immutable generation metadata is the forward-compatible root for future
80+
multi-address recovery. The legacy single-address materialization is still kept
81+
because the current branch restores the V0 one-address model directly.
82+
83+
That means the backup already contains the stable metadata that a future
84+
multi-address PR will need, while today's restore path can still recreate the
85+
existing legacy static address exactly.
86+
87+
## Encryption Model
88+
89+
The file is encrypted with `secretbox` using a symmetric key derived from lnd
90+
via `Signer.DeriveSharedKey`.
91+
92+
The derivation uses:
93+
94+
- a fixed NUMS public key
95+
- the static-address main key family
96+
- key index `0`
97+
98+
This ties backup decryption to the same lnd seed that controls the static
99+
address keys without introducing a user-managed recovery password in this
100+
phase.
101+
102+
## When Backups Are Written
103+
104+
The backup is only written once a complete recoverable generation exists.
105+
Today that means both of the following must already exist locally:
106+
107+
- a paid current `l402.token`
108+
- a local legacy static address bound to that token
109+
110+
Pending tokens are not backed up.
111+
112+
If the immutable backup file for the current paid token ID already exists,
113+
backup creation is a no-op.
114+
115+
## Startup Behavior
116+
117+
Startup is responsible for materializing the current generation before the
118+
backup is written.
119+
120+
On startup `loopd` now:
121+
122+
1. creates the recovery service
123+
2. if the install is fresh and immutable backup files already exist locally,
124+
restores the latest backup by filename timestamp
125+
3. otherwise, asks the static-address manager for the current static address
126+
4. if the address does not exist yet, fetches the paid L402, derives the
127+
client key, requests the legacy static address from the server, stores it,
128+
and imports the tapscript into lnd
129+
5. writes the immutable backup for the resulting paid-L402/static-address
130+
generation
131+
132+
This gives the branch the "one backup per L402" property without later backup
133+
refreshes.
134+
135+
### Existing Users
136+
137+
For existing users that already have a paid L402 and a legacy static address,
138+
the first startup with the upgraded client backfills the missing immutable
139+
backup for the active generation.
140+
141+
### Fresh Installs
142+
143+
For fresh installations, startup first checks whether immutable backups already
144+
exist in the active Loop data directory.
145+
146+
If they do, Loop restores the latest backup by filename timestamp instead of
147+
creating a new paid L402 generation.
148+
149+
If they do not, startup materializes the initial paid L402 plus legacy static-
150+
address generation so the backup can be written immediately.
151+
152+
The `loop static new` command is therefore no longer the only creation point.
153+
It now returns the current static address and only falls back to on-demand
154+
creation if startup initialization did not complete earlier.
155+
156+
## Restore Flow
157+
158+
`loop recover --backup_file <path>` restores a specific immutable backup. If
159+
`--backup_file` is omitted, Loop restores the most recent valid immutable
160+
backup in the active network directory.
161+
162+
Current restore performs the following steps:
163+
164+
1. derive the local encryption key from lnd
165+
2. read and decrypt the backup file
166+
3. validate the backup version, network, and L402 token ID
167+
4. restore the paid `l402.token` file if it is not already present with the
168+
same contents
169+
5. if legacy static-address metadata is present, reconstruct the client pubkey
170+
6. recreate the local legacy static-address record and re-import its tapscript
171+
into lnd
172+
7. trigger best-effort deposit reconciliation
173+
174+
Client-key reconstruction uses the following strategy:
175+
176+
- first try the exact backed-up key locator, including locator `0/0`
177+
- if that fails, scan a `+/-20` child-index window around the backed-up
178+
locator in the static-address main key family
179+
- when the backup contains the client pubkey, require that the derived key
180+
matches it before accepting the address reconstruction
181+
182+
The multi-address scan-and-rebuild flow is intentionally not activated in this
183+
branch yet. This branch only makes sure the immutable backup already contains
184+
the metadata that future flow will need.
185+
186+
## Future Multi-Address Generation
187+
188+
The planned multi-address model uses only two client-side key families:
189+
190+
- `main` addresses for externally visible static-address deposits
191+
- `change` addresses for outputs that return value back into the static-address
192+
address space
193+
194+
The future `static_addresses` table remains a table of concrete derived
195+
addresses. Each row represents one address child and stores:
196+
197+
- the client pubkey
198+
- the server pubkey
199+
- the client key family
200+
- the client key index
201+
- the resulting `pkScript`
202+
- the protocol version
203+
- the generation initiation height
204+
205+
The immutable backup does not store every row. Instead it stores the generation
206+
root metadata that allows those rows to be rediscovered by scanning.
207+
208+
For each future `main` or `change` address:
209+
210+
1. the client chooses the appropriate key family
211+
2. the client derives the next pubkey from lnd for that family
212+
3. the client combines that pubkey with the L402-bound server pubkey using the
213+
static-address MuSig2 construction for the backed-up protocol version
214+
4. the taproot tweak commits to the static-address timeout leaf
215+
5. the resulting taproot output key yields the final P2TR `pkScript`
216+
6. the concrete child row is stored locally in `static_addresses`
217+
218+
Because the backup is immutable, future restore must regenerate candidate
219+
`main` and `change` children from the backed-up branch metadata, rescan from
220+
the backed-up start height, and rebuild local table rows from what is found on
221+
chain. It must not depend on a mutable "last issued child index" snapshot.
222+
223+
## Server Proof For Multi-Address Inputs
224+
225+
For a future static swap or withdrawal that spends multi-address inputs, the
226+
server-side proof model is:
227+
228+
1. the paid L402 authenticates the request and identifies the generation
229+
2. the L402 selects the fixed generation server pubkey and the fixed
230+
protocol/expiry parameters
231+
3. for each input, the client sends the concrete client pubkey that was used to
232+
construct that input's address
233+
4. the server recomputes the timeout leaf for the backed-up protocol version
234+
and expiry
235+
5. the server recomputes the MuSig2 aggregate key from the concrete client
236+
pubkey for that input, the server pubkey bound to the L402 generation, and
237+
the taproot tweak implied by the timeout leaf
238+
6. the server derives the expected taproot output key and the expected P2TR
239+
`pkScript`
240+
7. the server compares that derived `pkScript` with the prevout `pkScript` of
241+
the input being authorized
242+
243+
If they match, the input belongs to that L402 generation because the output
244+
commits to the generation's server key and the concrete client pubkey used for
245+
that input.
246+
247+
This proof is about generation membership, not about proving a particular child
248+
index to the server. The immutable backup therefore only needs the stable
249+
generation root metadata, while exact row discovery remains a client-side
250+
wallet-and-chain scan problem.
251+
252+
## Operational Limits
253+
254+
Current restore still restores the legacy one-address model only.
255+
256+
Some practical consequences follow from that:
257+
258+
- restoring an older immutable backup is best done into a fresh Loop data
259+
directory
260+
- only one legacy static address can be recreated directly by the current
261+
restore code
262+
- historical deposit state is rebuilt best-effort from reconciliation, not by
263+
replaying every stored deposit transition
264+
265+
## Why The Backup Is Immutable
266+
267+
The multi-address work needs recovery to be based on stable root material, not
268+
on mutable local cursor snapshots.
269+
270+
Using one immutable backup per L402 forces that discipline now:
271+
272+
- the backup must describe a recoverable generation root
273+
- restore must be able to rediscover state from deterministic wallet- and
274+
chain-derived scanning
275+
- later address issuance must not depend on backup files being rewritten
276+
277+
That is the key design constraint for the next PRs.
278+
279+
## Package Boundaries
280+
281+
This package owns:
282+
283+
- backup payload definition
284+
- backup encryption and decryption
285+
- immutable backup-file discovery and selection
286+
- paid L402 token-file backup and restore
287+
- legacy static-address key re-derivation and restore orchestration
288+
- immutable generation metadata for future multi-address restore
289+
- post-restore deposit reconciliation orchestration
290+
291+
This package does not own:
292+
293+
- CLI command handling
294+
- gRPC transport
295+
- the static-address server protocol
296+
- the future multi-address scanning implementation
297+
- `loopd` startup wiring

0 commit comments

Comments
 (0)