Skip to content

Commit 239b90a

Browse files
committed
recover L402 and static address
1 parent f8f474e commit 239b90a

35 files changed

Lines changed: 5443 additions & 407 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
@@ -98,7 +98,8 @@ var (
9898
monitorCommand, quoteCommand, listAuthCommand, fetchL402Command,
9999
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
100100
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
101-
getInfoCommand, abandonSwapCommand, reservationsCommands,
101+
getInfoCommand, abandonSwapCommand, recoverCommand,
102+
reservationsCommands,
102103
instantOutCommand, listInstantOutsCommand, stopCommand,
103104
printManCommand, printMarkdownCommand,
104105
}

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
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{},
@@ -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
@@ -406,6 +406,16 @@ abandon a swap with a given swap hash
406406
.PP
407407
\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.
408408

409+
.SH recover
410+
.PP
411+
restore static address and L402 state from a local backup file
412+
413+
.PP
414+
\fB--backup_file\fP="": path to an encrypted backup file; if omitted, loopd selects and validates the latest active-network backup candidate
415+
416+
.PP
417+
\fB--help, -h\fP: show help
418+
409419
.SH reservations, r
410420
.PP
411421
manage reservations
@@ -459,7 +469,7 @@ perform on-chain to off-chain swaps using static addresses.
459469

460470
.SS new, n
461471
.PP
462-
Create a new static loop in address.
472+
Return the static loop in address.
463473

464474
.PP
465475
\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
@@ -37,6 +37,7 @@ require (
3737
github.com/urfave/cli-docs/v3 v3.1.1-0.20251020101624-bec07369b4f6
3838
github.com/urfave/cli/v3 v3.4.1
3939
go.etcd.io/bbolt v1.4.3
40+
golang.org/x/crypto v0.46.0
4041
golang.org/x/sync v0.19.0
4142
google.golang.org/grpc v1.79.3
4243
google.golang.org/protobuf v1.36.11
@@ -195,7 +196,6 @@ require (
195196
go.uber.org/multierr v1.6.0 // indirect
196197
go.uber.org/zap v1.24.0 // indirect
197198
go.yaml.in/yaml/v3 v3.0.4 // indirect
198-
golang.org/x/crypto v0.46.0 // indirect
199199
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
200200
golang.org/x/mod v0.30.0 // indirect
201201
golang.org/x/net v0.48.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"
@@ -597,13 +598,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
597598
withdrawalManager *withdraw.Manager
598599
openChannelManager *openchannel.Manager
599600
staticLoopInManager *loopin.Manager
601+
recoveryService *recovery.Service
600602
)
601603

602604
// Static address manager setup.
603605
staticAddressStore := address.NewSqlStore(baseDb)
604606
addrCfg := &address.ManagerConfig{
605607
AddressClient: staticAddressClient,
606-
FetchL402: swapClient.Server.FetchL402,
608+
FetchL402: func(ctx context.Context) error {
609+
return swapClient.Server.FetchL402(ctx)
610+
},
607611
Store: staticAddressStore,
608612
WalletKit: d.lnd.WalletKit,
609613
ChainParams: d.lnd.ChainParams,
@@ -705,6 +709,46 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
705709
return fmt.Errorf("unable to create loop-in manager: %w", err)
706710
}
707711

712+
// Keep startup restore/write-backup free of deposit reconciliation so we
713+
// don't create deposit FSMs before the deposit manager is running.
714+
startupRecoveryService := recovery.NewService(
715+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
716+
staticAddressManager, nil,
717+
)
718+
719+
restoreResult, restoredFromBackup, err :=
720+
startupRecoveryService.RestoreLatestOnFreshInstall(d.mainCtx)
721+
if err != nil {
722+
return fmt.Errorf("unable to restore latest recovery "+
723+
"backup on fresh install: %w", err)
724+
}
725+
if restoredFromBackup {
726+
infof("Restored fresh install from encrypted recovery "+
727+
"backup %s", restoreResult.BackupFile)
728+
} else {
729+
_, _, err = staticAddressManager.NewAddress(d.mainCtx)
730+
if err != nil {
731+
warnf("Unable to initialize static address generation "+
732+
"during startup: %v", err)
733+
}
734+
}
735+
736+
backupFile, err := startupRecoveryService.WriteBackup(d.mainCtx)
737+
if err != nil {
738+
warnf("Unable to write startup recovery backup: %v", err)
739+
}
740+
if backupFile != "" {
741+
infof("Wrote encrypted recovery backup to %s after "+
742+
"initializing the current L402 generation", backupFile)
743+
}
744+
745+
// Runtime recovery is wired with the deposit manager so explicit
746+
// recovery RPCs can reconcile restored static-address deposits.
747+
recoveryService = recovery.NewService(
748+
d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit,
749+
staticAddressManager, depositManager,
750+
)
751+
708752
var (
709753
reservationManager *reservation.Manager
710754
instantOutManager *instantout.Manager
@@ -773,6 +817,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
773817
staticLoopInManager: staticLoopInManager,
774818
openChannelManager: openChannelManager,
775819
assetClient: d.assetClient,
820+
recoveryService: recoveryService,
776821
stopDaemon: d.Stop,
777822
}
778823

0 commit comments

Comments
 (0)