Skip to content

Commit 405e610

Browse files
committed
recover L402 and static address
1 parent 12c06a5 commit 405e610

31 files changed

Lines changed: 5092 additions & 373 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,28 @@ To execute a Loop In:
6464
loop in <amt_in_satoshis>
6565
```
6666

67+
### Static Address Recovery
68+
Loop now keeps one encrypted immutable recovery backup per paid L402
69+
generation in the active network data directory. Each backup contains the raw
70+
paid `l402.token` plus the legacy static-address parameters needed to recreate
71+
the current address locally.
72+
73+
The backup is encrypted with a key derived by the backing `lnd` wallet. It is
74+
therefore only useful with that same `lnd` instance, or with an `lnd` restored
75+
from the same seed/key material. The Loop backup file alone is not enough to
76+
recover static-address access.
77+
78+
Existing static-address users get this backup backfilled on the next startup
79+
with the upgraded client. Fresh installs materialize the initial paid-L402 and
80+
legacy static-address generation during startup so the first backup can be
81+
written immediately.
82+
83+
The follow-up multi-address work is expected to keep this one-backup-per-L402
84+
model and extend it with deterministic root metadata for synthetic-xpub /
85+
BIP328-style address derivation and scanning. See
86+
[recovery/README.md](./recovery/README.md) for the full recovery model and the
87+
planned multi-address outlook.
88+
6789
### More info
6890

6991
- [Loop FAQs](./docs/faqs.md)

cmd/loop/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ var (
8888
monitorCommand, quoteCommand, listAuthCommand, fetchL402Command,
8989
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
9090
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
91-
getInfoCommand, abandonSwapCommand, reservationsCommands,
91+
getInfoCommand, abandonSwapCommand, recoverCommand,
92+
reservationsCommands,
9293
instantOutCommand, listInstantOutsCommand, stopCommand,
9394
printManCommand, printMarkdownCommand,
9495
}

cmd/loop/recover.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"context"
5+
6+
"github.com/lightninglabs/loop/looprpc"
7+
"github.com/urfave/cli/v3"
8+
)
9+
10+
var recoverCommand = &cli.Command{
11+
Name: "recover",
12+
Usage: "restore static address and L402 state from a local backup file",
13+
Description: "Restores the local static-address state and L402 token " +
14+
"from an encrypted backup file. If --backup_file is omitted, " +
15+
"loopd will use the most recent immutable network-specific " +
16+
"L402 backup file.",
17+
Flags: []cli.Flag{
18+
&cli.StringFlag{
19+
Name: "backup_file",
20+
Usage: "path to an encrypted backup file; if omitted, " +
21+
"loopd uses the most recent immutable L402 " +
22+
"backup file path",
23+
},
24+
},
25+
Action: runRecover,
26+
}
27+
28+
func runRecover(ctx context.Context, cmd *cli.Command) error {
29+
if cmd.NArg() > 0 {
30+
return showCommandHelp(ctx, cmd)
31+
}
32+
33+
client, cleanup, err := getClient(cmd)
34+
if err != nil {
35+
return err
36+
}
37+
defer cleanup()
38+
39+
resp, err := client.Recover(
40+
ctx, &looprpc.RecoverRequest{
41+
BackupFile: cmd.String("backup_file"),
42+
},
43+
)
44+
if err != nil {
45+
return err
46+
}
47+
48+
printRespJSON(resp)
49+
return nil
50+
}

cmd/loop/staticaddr.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/lightninglabs/loop/labels"
1111
"github.com/lightninglabs/loop/looprpc"
12+
"github.com/lightninglabs/loop/staticaddr/address"
1213
"github.com/lightninglabs/loop/staticaddr/loopin"
1314
"github.com/lightninglabs/loop/swapserverrpc"
1415
lndcommands "github.com/lightningnetwork/lnd/cmd/commands"
@@ -43,13 +44,15 @@ var staticAddressCommands = &cli.Command{
4344
var newStaticAddressCommand = &cli.Command{
4445
Name: "new",
4546
Aliases: []string{"n"},
46-
Usage: "Create a new static loop in address.",
47+
Usage: "Return the static loop in address.",
4748
Description: `
48-
Requests a new static loop in address from the server. Funds that are
49-
sent to this address will be locked by a 2:2 multisig between us and the
50-
loop server, or a timeout path that we can sweep once it opens up. The
51-
funds can either be cooperatively spent with a signature from the server
52-
or looped in.
49+
Returns the current static loop in address. On a fresh installation loopd
50+
initializes the current static-address generation during startup. If the
51+
address is still missing, this call will create it on demand. Funds sent
52+
to the address will be locked by a 2:2 multisig between us and the loop
53+
server, or a timeout path that we can sweep once it opens up. The funds
54+
can either be cooperatively spent with a signature from the server or
55+
looped in.
5356
`,
5457
Action: newStaticAddress,
5558
}
@@ -59,16 +62,16 @@ func newStaticAddress(ctx context.Context, cmd *cli.Command) error {
5962
return showCommandHelp(ctx, cmd)
6063
}
6164

62-
err := displayNewAddressWarning()
65+
client, cleanup, err := getClient(cmd)
6366
if err != nil {
6467
return err
6568
}
69+
defer cleanup()
6670

67-
client, cleanup, err := getClient(cmd)
71+
err = maybeDisplayNewAddressWarning(ctx, client)
6872
if err != nil {
6973
return err
7074
}
71-
defer cleanup()
7275

7376
resp, err := client.NewStaticAddress(
7477
ctx, &looprpc.NewStaticAddressRequest{},
@@ -841,8 +844,26 @@ func lowConfDepositWarning(allDeposits []*looprpc.Deposit,
841844
)
842845
}
843846

847+
func maybeDisplayNewAddressWarning(ctx context.Context,
848+
client looprpc.SwapClientClient) error {
849+
850+
_, err := client.GetStaticAddressSummary(
851+
ctx, &looprpc.StaticAddressSummaryRequest{},
852+
)
853+
switch {
854+
case err == nil:
855+
return nil
856+
857+
case strings.Contains(err.Error(), address.ErrNoStaticAddress.Error()):
858+
return displayNewAddressWarning()
859+
860+
default:
861+
return nil
862+
}
863+
}
864+
844865
func displayNewAddressWarning() error {
845-
fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " +
866+
fmt.Printf("\nWARNING: Be aware that losing your l402.token file in " +
846867
".loop under your home directory will take your ability to " +
847868
"spend funds sent to the static address via loop-ins or " +
848869
"withdrawals. You will have to wait until the deposit " +

docs/loop.1

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ abandon a swap with a given swap hash
403403
.PP
404404
\fB--i_know_what_i_am_doing\fP: Specify this flag if you made sure that you read and understood the following consequence of applying this command.
405405

406+
.SH recover
407+
.PP
408+
restore static address and L402 state from a local backup file
409+
410+
.PP
411+
\fB--backup_file\fP="": path to an encrypted backup file; if omitted, loopd uses the most recent immutable L402 backup file path
412+
413+
.PP
414+
\fB--help, -h\fP: show help
415+
406416
.SH reservations, r
407417
.PP
408418
manage reservations
@@ -456,7 +466,7 @@ perform on-chain to off-chain swaps using static addresses.
456466

457467
.SS new, n
458468
.PP
459-
Create a new static loop in address.
469+
Return the static loop in address.
460470

461471
.PP
462472
\fB--help, -h\fP: show help

docs/loop.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,25 @@ The following flags are supported:
418418
| `--i_know_what_i_am_doing` | Specify this flag if you made sure that you read and understood the following consequence of applying this command | bool | `false` |
419419
| `--help` (`-h`) | show help | bool | `false` |
420420

421+
### `recover` command
422+
423+
restore static address and L402 state from a local backup file.
424+
425+
Restores the local static-address state and L402 token from an encrypted backup file. If --backup_file is omitted, loopd will use the most recent immutable network-specific L402 backup file.
426+
427+
Usage:
428+
429+
```bash
430+
$ loop [GLOBAL FLAGS] recover [COMMAND FLAGS] [ARGUMENTS...]
431+
```
432+
433+
The following flags are supported:
434+
435+
| Name | Description | Type | Default value |
436+
|---------------------|----------------------------------------------------------------------------------------------------------|--------|:-------------:|
437+
| `--backup_file="…"` | path to an encrypted backup file; if omitted, loopd uses the most recent immutable L402 backup file path | string |
438+
| `--help` (`-h`) | show help | bool | `false` |
439+
421440
### `reservations` command (aliases: `r`)
422441

423442
manage reservations.
@@ -529,9 +548,9 @@ The following flags are supported:
529548

530549
### `static new` subcommand (aliases: `n`)
531550

532-
Create a new static loop in address.
551+
Return the static loop in address.
533552

534-
Requests a new static loop in address from the server. Funds that are sent to this address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in.
553+
Returns the current static loop in address. On a fresh installation loopd initializes the current static-address generation during startup. If the address is still missing, this call will create it on demand. Funds sent to the address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in.
535554

536555
Usage:
537556

loopd/daemon.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/lightninglabs/loop/loopdb"
2323
loop_looprpc "github.com/lightninglabs/loop/looprpc"
2424
"github.com/lightninglabs/loop/notifications"
25+
"github.com/lightninglabs/loop/recovery"
2526
"github.com/lightninglabs/loop/staticaddr/address"
2627
"github.com/lightninglabs/loop/staticaddr/deposit"
2728
"github.com/lightninglabs/loop/staticaddr/loopin"
@@ -577,13 +578,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
577578
withdrawalManager *withdraw.Manager
578579
openChannelManager *openchannel.Manager
579580
staticLoopInManager *loopin.Manager
581+
recoveryService *recovery.Service
580582
)
581583

582584
// Static address manager setup.
583585
staticAddressStore := address.NewSqlStore(baseDb)
584586
addrCfg := &address.ManagerConfig{
585587
AddressClient: staticAddressClient,
586-
FetchL402: swapClient.Server.FetchL402,
588+
FetchL402: func(ctx context.Context) error {
589+
return swapClient.Server.FetchL402(ctx)
590+
},
587591
Store: staticAddressStore,
588592
WalletKit: d.lnd.WalletKit,
589593
ChainParams: d.lnd.ChainParams,
@@ -691,6 +695,44 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
691695
return fmt.Errorf("unable to create loop-in manager: %w", err)
692696
}
693697

698+
// Keep startup restore/write-backup free of deposit reconciliation so we
699+
// don't create deposit FSMs before the deposit manager is running.
700+
startupRecoveryService := recovery.NewService(
701+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
702+
staticAddressManager, nil,
703+
)
704+
705+
restoreResult, restoredFromBackup, err :=
706+
startupRecoveryService.RestoreLatestOnFreshInstall(d.mainCtx)
707+
if err != nil {
708+
return fmt.Errorf("unable to restore latest recovery "+
709+
"backup on fresh install: %w", err)
710+
}
711+
if restoredFromBackup {
712+
infof("Restored fresh install from encrypted recovery "+
713+
"backup %s", restoreResult.BackupFile)
714+
} else {
715+
_, _, err = staticAddressManager.NewAddress(d.mainCtx)
716+
if err != nil {
717+
warnf("Unable to initialize static address generation "+
718+
"during startup: %v", err)
719+
}
720+
}
721+
722+
backupFile, err := startupRecoveryService.WriteBackup(d.mainCtx)
723+
if err != nil {
724+
warnf("Unable to write startup recovery backup: %v", err)
725+
}
726+
if backupFile != "" {
727+
infof("Wrote encrypted recovery backup to %s after "+
728+
"initializing the current L402 generation", backupFile)
729+
}
730+
731+
recoveryService = recovery.NewService(
732+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
733+
staticAddressManager, depositManager,
734+
)
735+
694736
var (
695737
reservationManager *reservation.Manager
696738
instantOutManager *instantout.Manager
@@ -755,6 +797,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
755797
staticLoopInManager: staticLoopInManager,
756798
openChannelManager: openChannelManager,
757799
assetClient: d.assetClient,
800+
recoveryService: recoveryService,
758801
stopDaemon: d.Stop,
759802
}
760803

loopd/swapclient_server.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/lightninglabs/loop/liquidity"
3030
"github.com/lightninglabs/loop/loopdb"
3131
"github.com/lightninglabs/loop/looprpc"
32+
"github.com/lightninglabs/loop/recovery"
3233
"github.com/lightninglabs/loop/staticaddr/address"
3334
"github.com/lightninglabs/loop/staticaddr/deposit"
3435
"github.com/lightninglabs/loop/staticaddr/loopin"
@@ -101,6 +102,7 @@ type swapClientServer struct {
101102
staticLoopInManager *loopin.Manager
102103
openChannelManager *openchannel.Manager
103104
assetClient *assets.TapdClient
105+
recoveryService *recovery.Service
104106
swaps map[lntypes.Hash]loop.SwapInfo
105107
subscribers map[int]chan<- any
106108
statusChan chan loop.SwapInfo
@@ -1277,6 +1279,32 @@ func (s *swapClientServer) FetchL402Token(ctx context.Context,
12771279
return &looprpc.FetchL402TokenResponse{}, nil
12781280
}
12791281

1282+
// Recover restores the local paid L402 token material and static-address state
1283+
// from an encrypted backup file.
1284+
func (s *swapClientServer) Recover(ctx context.Context,
1285+
req *looprpc.RecoverRequest) (*looprpc.RecoverResponse, error) {
1286+
1287+
if s.recoveryService == nil {
1288+
return nil, status.Error(
1289+
codes.Unavailable, "recovery service not configured",
1290+
)
1291+
}
1292+
1293+
result, err := s.recoveryService.Restore(ctx, req.GetBackupFile())
1294+
if err != nil {
1295+
return nil, err
1296+
}
1297+
1298+
return &looprpc.RecoverResponse{
1299+
BackupFile: result.BackupFile,
1300+
RestoredL402: result.RestoredL402,
1301+
RestoredStaticAddress: result.RestoredStaticAddress,
1302+
StaticAddress: result.StaticAddress,
1303+
NumDepositsFound: uint32(result.NumDepositsFound),
1304+
DepositReconciliationError: result.DepositReconciliationError,
1305+
}, nil
1306+
}
1307+
12801308
// GetInfo returns basic information about the loop daemon and details to swaps
12811309
// from the swap store.
12821310
func (s *swapClientServer) GetInfo(ctx context.Context,
@@ -1646,6 +1674,17 @@ func (s *swapClientServer) NewStaticAddress(ctx context.Context,
16461674
return nil, err
16471675
}
16481676

1677+
if s.recoveryService != nil {
1678+
backupFile, backupErr := s.recoveryService.WriteBackup(ctx)
1679+
if backupErr != nil {
1680+
warnf("Unable to write recovery backup after static "+
1681+
"address request: %v", backupErr)
1682+
} else if backupFile != "" {
1683+
infof("Wrote encrypted recovery backup to %s after "+
1684+
"static address request", backupFile)
1685+
}
1686+
}
1687+
16491688
return &looprpc.NewStaticAddressResponse{
16501689
Address: staticAddress.String(),
16511690
Expiry: uint32(expiry),

0 commit comments

Comments
 (0)