Skip to content

Commit b018815

Browse files
committed
recover L402 and static address
1 parent fc506ac commit b018815

35 files changed

Lines changed: 5425 additions & 389 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 at most one encrypted immutable recovery backup per paid L402
69+
generation in the active network data directory. A backup is written only after
70+
Loop has both the paid `l402.token` and the concrete static-address parameters
71+
for that generation.
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. A fresh install that has no local L402 or
80+
static-address state first checks for an existing recovery backup in the active
81+
network directory. If none is restored, startup materializes the initial
82+
paid-L402/static-address generation and writes its backup.
83+
84+
The follow-up multi-address work is expected to keep this one-backup-per-L402
85+
model and use the deterministic receive/change key-family metadata already
86+
stored in the backup. See [recovery/README.md](./recovery/README.md) for the
87+
full recovery model and the 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
@@ -97,7 +97,8 @@ var (
9797
monitorCommand, quoteCommand, listAuthCommand, fetchL402Command,
9898
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
9999
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
100-
getInfoCommand, abandonSwapCommand, reservationsCommands,
100+
getInfoCommand, abandonSwapCommand, recoverCommand,
101+
reservationsCommands,
101102
instantOutCommand, listInstantOutsCommand, stopCommand,
102103
printManCommand, printMarkdownCommand,
103104
}

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 selects the latest decryptable active-network backup " +
16+
"candidate and fully validates it before restoring state.",
17+
Flags: []cli.Flag{
18+
&cli.StringFlag{
19+
Name: "backup_file",
20+
Usage: "path to an encrypted backup file; if omitted, " +
21+
"loopd selects and validates the latest active-network " +
22+
"backup candidate",
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
"github.com/lightningnetwork/lnd"
@@ -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{},
@@ -846,8 +849,26 @@ func lowConfDepositWarning(allDeposits []*looprpc.Deposit,
846849
)
847850
}
848851

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

cmd/loop/testdata/sessions/static-loop-in/01_loop-static-new.json

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,36 @@
1313
"clock_start_unix": 1769407086
1414
},
1515
"events": [
16+
{
17+
"time_ms": 1,
18+
"kind": "grpc",
19+
"data": {
20+
"method": "/looprpc.SwapClient/GetStaticAddressSummary",
21+
"event": "request",
22+
"message_type": "looprpc.StaticAddressSummaryRequest",
23+
"payload": {}
24+
}
25+
},
26+
{
27+
"time_ms": 1,
28+
"kind": "grpc",
29+
"data": {
30+
"method": "/looprpc.SwapClient/GetStaticAddressSummary",
31+
"event": "error",
32+
"error": "rpc error: code = Unknown desc = no static address parameters found",
33+
"status": {
34+
"code": 2,
35+
"message": "no static address parameters found"
36+
}
37+
}
38+
},
1639
{
1740
"time_ms": 1,
1841
"kind": "stdout",
1942
"data": {
2043
"lines": [
2144
"\n",
22-
"WARNING: Be aware that loosing your l402.token file in .loop under your home directory will take your ability to spend funds sent to the static address via loop-ins or withdrawals. You will have to wait until the deposit expires and your loop client sweeps the funds back to your lnd wallet. The deposit expiry could be months in the future.\n",
45+
"WARNING: Be aware that losing your l402.token file in .loop under your home directory will take your ability to spend funds sent to the static address via loop-ins or withdrawals. You will have to wait until the deposit expires and your loop client sweeps the funds back to your lnd wallet. The deposit expiry could be months in the future.\n",
2346
"\n",
2447
"CONTINUE WITH NEW ADDRESS? (y/n): "
2548
]

cmd/loop/testdata/sessions/static-loop-in/04_loop-static.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
" loop static [command [command options]]\n",
2525
"\n",
2626
"COMMANDS:\n",
27-
" new, n Create a new static loop in address.\n",
27+
" new, n Return the static loop in address.\n",
2828
" listunspent, l List unspent static address outputs.\n",
2929
" listdeposits Displays static address deposits. A filter can be applied to only show deposits in a specific state.\n",
3030
" listwithdrawals Display a summary of past withdrawals.\n",

docs/loop.1

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,16 @@ abandon a swap with a given swap hash
396396
.PP
397397
\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.
398398

399+
.SH recover
400+
.PP
401+
restore static address and L402 state from a local backup file
402+
403+
.PP
404+
\fB--backup_file\fP="": path to an encrypted backup file; if omitted, loopd selects and validates the latest active-network backup candidate
405+
406+
.PP
407+
\fB--help, -h\fP: show help
408+
399409
.SH reservations, r
400410
.PP
401411
manage reservations
@@ -449,7 +459,7 @@ perform on-chain to off-chain swaps using static addresses.
449459

450460
.SS new, n
451461
.PP
452-
Create a new static loop in address.
462+
Return the static loop in address.
453463

454464
.PP
455465
\fB--help, -h\fP: show help

docs/loop.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,25 @@ The following flags are supported:
419419
| `--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` |
420420
| `--help` (`-h`) | show help | bool | `false` |
421421

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

424443
manage reservations.
@@ -530,9 +549,9 @@ The following flags are supported:
530549

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

533-
Create a new static loop in address.
552+
Return the static loop in address.
534553

535-
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.
554+
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.
536555

537556
Usage:
538557

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ require (
191191
go.uber.org/multierr v1.6.0 // indirect
192192
go.uber.org/zap v1.24.0 // indirect
193193
go.yaml.in/yaml/v3 v3.0.4 // indirect
194-
golang.org/x/crypto v0.47.0 // indirect
194+
golang.org/x/crypto v0.47.0
195195
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect
196196
golang.org/x/mod v0.31.0 // indirect
197197
golang.org/x/net v0.49.0 // indirect

loopd/daemon.go

Lines changed: 46 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"
@@ -618,13 +619,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
618619
withdrawalManager *withdraw.Manager
619620
openChannelManager *openchannel.Manager
620621
staticLoopInManager *loopin.Manager
622+
recoveryService *recovery.Service
621623
)
622624

623625
// Static address manager setup.
624626
staticAddressStore := address.NewSqlStore(baseDb)
625627
addrCfg := &address.ManagerConfig{
626628
AddressClient: staticAddressClient,
627-
FetchL402: swapClient.Server.FetchL402,
629+
FetchL402: func(ctx context.Context) error {
630+
return swapClient.Server.FetchL402(ctx)
631+
},
628632
Store: staticAddressStore,
629633
WalletKit: d.lnd.WalletKit,
630634
ChainParams: d.lnd.ChainParams,
@@ -726,6 +730,46 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
726730
return fmt.Errorf("unable to create loop-in manager: %w", err)
727731
}
728732

733+
// Keep startup restore/write-backup free of deposit reconciliation so we
734+
// don't create deposit FSMs before the deposit manager is running.
735+
startupRecoveryService := recovery.NewService(
736+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
737+
staticAddressManager, nil,
738+
)
739+
740+
restoreResult, restoredFromBackup, err :=
741+
startupRecoveryService.RestoreLatestOnFreshInstall(d.mainCtx)
742+
if err != nil {
743+
return fmt.Errorf("unable to restore latest recovery "+
744+
"backup on fresh install: %w", err)
745+
}
746+
if restoredFromBackup {
747+
infof("Restored fresh install from encrypted recovery "+
748+
"backup %s", restoreResult.BackupFile)
749+
} else {
750+
_, _, err = staticAddressManager.NewAddress(d.mainCtx)
751+
if err != nil {
752+
warnf("Unable to initialize static address generation "+
753+
"during startup: %v", err)
754+
}
755+
}
756+
757+
backupFile, err := startupRecoveryService.WriteBackup(d.mainCtx)
758+
if err != nil {
759+
warnf("Unable to write startup recovery backup: %v", err)
760+
}
761+
if backupFile != "" {
762+
infof("Wrote encrypted recovery backup to %s after "+
763+
"initializing the current L402 generation", backupFile)
764+
}
765+
766+
// Runtime recovery is wired with the deposit manager so explicit
767+
// recovery RPCs can reconcile restored static-address deposits.
768+
recoveryService = recovery.NewService(
769+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
770+
staticAddressManager, depositManager,
771+
)
772+
729773
var (
730774
reservationManager *reservation.Manager
731775
instantOutManager *instantout.Manager
@@ -794,6 +838,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
794838
staticLoopInManager: staticLoopInManager,
795839
openChannelManager: openChannelManager,
796840
assetClient: d.assetClient,
841+
recoveryService: recoveryService,
797842
stopDaemon: d.Stop,
798843
}
799844

0 commit comments

Comments
 (0)