Skip to content

Commit 7ae527e

Browse files
committed
recovery: restore multi-address static deposits
1 parent 827b3f0 commit 7ae527e

6 files changed

Lines changed: 617 additions & 53 deletions

File tree

loopd/daemon.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,9 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
712712
infof("Restored fresh install from encrypted recovery "+
713713
"backup %s", restoreResult.BackupFile)
714714
} else {
715-
_, _, err = staticAddressManager.NewAddress(d.mainCtx)
715+
_, err = staticAddressManager.EnsureStaticAddressSeed(
716+
d.mainCtx,
717+
)
716718
if err != nil {
717719
warnf("Unable to initialize static address generation "+
718720
"during startup: %v", err)

recovery/README.md

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ matching wallet child and recreate the one concrete static-address row.
7070
Current V0 backups initialize `legacy_first_height` from the legacy concrete
7171
static-address initiation height and `multi_address_first_height` from the
7272
current block height when the backup is written. They are separate fields so
73-
the future multi-address scan floor is independent from the legacy concrete
74-
address import hint.
73+
the multi-address scan floor is independent of the legacy concrete address
74+
import hint.
7575

7676
The Taproot address string, `pkScript`, and scan lookahead/gap limit are not
7777
backed up. The address and `pkScript` can be derived from the stored key and
@@ -88,15 +88,16 @@ are not replayed from the backup.
8888

8989
## Why Root And Legacy Fields Are Both Stored
9090

91-
The server pubkey, protocol version, expiry, planned multi-address
92-
receive/change key families, Bitcoin network, and multi-address first height
93-
define the stable fields future restore code will combine with lnd-derived
94-
client keys and a chain scan. They are not used by the current V0 restore path.
91+
The server pubkey, protocol version, expiry, multi-address receive/change key
92+
families, Bitcoin network, and multi-address first height are stable for the
93+
L402 generation. Multi-address restore combines those fields with lnd-derived
94+
client keys and matches the resulting scripts against wallet-visible UTXOs.
9595

96-
The current V0 restore path recreates the existing concrete address row
97-
directly, so the backup also stores that row's client pubkey, legacy client key
98-
family, and legacy first height. Those fields let restore find the matching
99-
local wallet child and import the concrete address from the right chain height.
96+
The legacy concrete address predates the receive/change branches and is restored
97+
as one explicit row. The backup therefore also stores that row's client pubkey,
98+
legacy client key family, and legacy first height, which let restore find the
99+
matching local wallet child and import the concrete address from the right
100+
height.
100101

101102
## Encryption Model
102103

@@ -196,22 +197,25 @@ Restore performs the following steps:
196197
existing file has identical contents
197198
7. import the tapscript into lnd and create or reuse the local concrete
198199
static-address record
199-
8. if static-address restore fails after token files were written, remove only
200+
8. scan the multi-address receive/change branches against wallet-visible UTXOs
201+
and recreate matching concrete address rows
202+
9. if an address restore phase fails after token files were written, remove only
200203
the token files written by this restore attempt
201-
9. trigger best-effort deposit reconciliation
204+
10. trigger best-effort deposit reconciliation
202205

203206
Client-key reconstruction uses the following strategy:
204207

205208
- scan child indexes `0` through `20` in the legacy static-address client key
206209
family using `DeriveKey`
207210
- accept the child whose derived pubkey matches the backed-up client pubkey
208211

209-
The multi-address scan-and-rebuild flow is not active yet. The immutable backup
210-
already contains the address-space metadata that flow will need.
212+
Multi-address scanning uses the immutable address-space fields in the backup to
213+
derive receive/change candidates without requiring a backed-up last-issued child
214+
index.
211215

212-
## Future Multi-Address Generation
216+
## Multi-Address Generation
213217

214-
The planned multi-address model uses two dedicated client-side key families:
218+
The multi-address model uses two dedicated client-side key families:
215219

216220
- `swap.StaticMultiAddressKeyFamily` for externally visible static-address
217221
deposits
@@ -221,8 +225,8 @@ The planned multi-address model uses two dedicated client-side key families:
221225
The legacy `swap.StaticAddressKeyFamily` remains the V0 concrete static-address
222226
family and the static-address HTLC key family.
223227

224-
The future `static_addresses` table remains a table of concrete derived
225-
addresses. Each row represents one address child and stores:
228+
The `static_addresses` table remains a table of concrete derived addresses.
229+
Each row represents one address child and stores:
226230

227231
- the client pubkey
228232
- the server pubkey
@@ -235,7 +239,7 @@ addresses. Each row represents one address child and stores:
235239
The immutable backup does not store every row. Instead it stores the
236240
address-space metadata that allows those rows to be rediscovered by scanning.
237241

238-
For each future receive or change address:
242+
For each receive or change address:
239243

240244
1. the client chooses the appropriate key family
241245
2. the client derives the next pubkey from lnd for that family
@@ -248,12 +252,19 @@ For each future receive or change address:
248252
The client key used in the MuSig2 aggregate key should also be the client's key
249253
in the timeout path for that concrete multi-address output.
250254

251-
Because the backup is immutable, future restore must regenerate candidate
252-
receive and change children from the backed-up key families and the restored
253-
lnd key material, rescan from the backed-up multi-address first height, and
254-
rebuild local table rows from what is found on chain. The lookahead/gap limit
255-
used during that scan is a restore parameter, not immutable backup data. Restore
256-
must not depend on a mutable "last issued child index" snapshot.
255+
Because the backup is immutable, restore regenerates candidate receive and
256+
change children from the backed-up branch fields and matches their scripts
257+
against lnd's wallet-visible UTXOs. The scan uses a rolling gap limit: each
258+
matched child resets the unused counter, and restore stops only after a full
259+
gap of consecutive unused children. The default gap is restore policy, not
260+
immutable backup data, so restore does not depend on a mutable "last issued
261+
child index" snapshot.
262+
263+
The multi-address branch scan takes one unfiltered `ListUnspent` snapshot from
264+
lnd and matches all derived candidates against that in-memory script set.
265+
Deposit reconciliation runs after matching and performs its own `ListUnspent`
266+
pass through the address manager so it can use the newly active address rows and
267+
confirmation metadata.
257268

258269
## Server Proof For Multi-Address Inputs
259270

@@ -286,18 +297,19 @@ and chain-scan problem.
286297

287298
## Operational Limits
288299

289-
Restore in this implementation recreates the V0 one-address model only.
300+
Restore in this implementation rebuilds current wallet-visible static-address
301+
state, not the full historical database.
290302

291303
Some practical consequences follow from that:
292304

293305
- restoring an older immutable backup is best done into a fresh Loop data
294-
directory, or into a directory that already contains the same token and
295-
static-address row
296-
- only one concrete static address can be recreated directly by this restore
297-
code
298-
- conflicting local `l402.token` contents or a different existing
299-
static-address row cause restore to fail rather than overwrite local state
300-
- active deposits are rebuilt best-effort from wallet reconciliation, not by
306+
directory
307+
- the legacy concrete static address is restored directly
308+
- multi-address receive/change rows are recreated for wallet-visible unspent
309+
outputs discovered by the rolling branch scan
310+
- conflicting local `l402.token` contents or different existing address rows
311+
cause restore to fail rather than overwrite local state
312+
- historical deposit state is rebuilt best-effort from reconciliation, not by
301313
replaying every stored deposit transition
302314

303315
## Why The Backup Is Immutable
@@ -324,13 +336,13 @@ This package owns:
324336
- immutable backup-file discovery and selection
325337
- paid L402 token-file backup and restore
326338
- V0 static-address key re-derivation and restore orchestration
327-
- static-address metadata fields for future multi-address restore
339+
- static-address root fields for multi-address restore
340+
- multi-address branch scanning against wallet-visible UTXOs
328341
- post-restore deposit reconciliation orchestration
329342

330343
This package does not own:
331344

332345
- CLI command handling
333346
- gRPC transport
334347
- the static-address server protocol
335-
- the future multi-address scanning implementation
336348
- `loopd` startup wiring

0 commit comments

Comments
 (0)