This commit is contained in:
Slyghtning 2026-08-12 09:06:18 +00:00 committed by GitHub
commit bff4cfb229
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 4393 additions and 682 deletions

View file

@ -4,11 +4,14 @@ import (
"context"
"errors"
"fmt"
"math"
"os"
"sort"
"strings"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd"
@ -17,6 +20,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
func init() {
@ -29,6 +33,7 @@ var staticAddressCommands = &cli.Command{
Usage: "perform on-chain to off-chain swaps using static addresses.",
Commands: []*cli.Command{
newStaticAddressCommand,
depositStaticAddressCommand,
listUnspentCommand,
listDepositsCommand,
listWithdrawalsCommand,
@ -45,15 +50,99 @@ var newStaticAddressCommand = &cli.Command{
Aliases: []string{"n"},
Usage: "Create a new static loop in address.",
Description: `
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.
Creates a new static loop in address. On a fresh installation loopd
initializes the static-address generation during startup. 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.
`,
Action: newStaticAddress,
}
var depositStaticAddressCommand = &cli.Command{
Name: "deposit",
Usage: "Create and fund a new static loop in address.",
Description: `
Creates a new static loop in address and initiates a deposit by calling
lnd's SendCoins API with the newly created address as the destination.
`,
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "amt",
Usage: "the number of bitcoin denominated in satoshis " +
"to send to the new static address",
},
&cli.BoolFlag{
Name: "sweepall",
Usage: "if set, then the amount field should be " +
"unset. This indicates that the wallet will " +
"attempt to sweep all outputs within the " +
"wallet or all funds in selected utxos (when " +
"supplied) to the new static address",
},
&cli.Int64Flag{
Name: "conf_target",
Usage: "(optional) the number of blocks that the " +
"funding transaction should confirm in, will " +
"be used for fee estimation",
},
&cli.Int64Flag{
Name: "sat_per_byte",
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
&cli.Uint64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
"the funding transaction",
},
&cli.Uint64Flag{
Name: "min_confs",
Usage: "(optional) the minimum number of confirmations " +
"each one of your outputs used for the funding " +
"transaction must satisfy",
Value: defaultUtxoMinConf,
},
&cli.BoolFlag{
Name: "force, f",
Usage: "if set, the funding transaction will be " +
"broadcast without asking for confirmation",
},
staticAddressCoinSelectionStrategyFlag,
&cli.StringSliceFlag{
Name: "utxo",
Usage: "a utxo specified as outpoint(tx:idx) which " +
"will be used as input for the funding " +
"transaction. This flag can be repeatedly used " +
"to specify multiple utxos as inputs. The " +
"selected utxos can either be entirely spent " +
"by specifying the sweepall flag or a specified " +
"amount can be spent in the utxos through " +
"the amt flag",
},
staticAddressFundingLabelFlag,
},
Action: depositStaticAddress,
}
var (
staticAddressCoinSelectionStrategyFlag = &cli.StringFlag{
Name: "coin_selection_strategy",
Usage: "(optional) the strategy to use for selecting coins. " +
"Possible values are 'largest', 'random', or " +
"'global-config'. If either 'largest' or 'random' is " +
"specified, it will override the globally configured " +
"strategy in lnd.conf",
Value: "global-config",
}
staticAddressFundingLabelFlag = &cli.StringFlag{
Name: "label",
Usage: "(optional) a label for the funding transaction",
}
)
func newStaticAddress(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() > 0 {
return showCommandHelp(ctx, cmd)
@ -82,6 +171,186 @@ func newStaticAddress(ctx context.Context, cmd *cli.Command) error {
return nil
}
func depositStaticAddress(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() > 0 {
return showCommandHelp(ctx, cmd)
}
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()
req, err := staticAddressDepositRequest(cmd, "")
if err != nil {
return err
}
err = maybeDisplayNewAddressWarning(ctx, client)
if err != nil {
return err
}
addrResp, err := client.NewStaticAddress(
ctx, &looprpc.NewStaticAddressRequest{},
)
if err != nil {
return err
}
req.GetSendCoinsRequest().Addr = addrResp.Address
if !(cmd.Bool("force") || cmd.Bool("f")) &&
term.IsTerminal(int(os.Stdout.Fd())) {
if !confirmStaticAddressDeposit(req, addrResp.Address) {
return nil
}
}
resp, err := client.NewStaticAddress(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
func staticAddressDepositRequest(
cmd *cli.Command, addr string) (*looprpc.NewStaticAddressRequest, error) {
if !cmd.IsSet("amt") && !cmd.Bool("sweepall") {
return nil, errors.New("amount argument missing")
}
amount := cmd.Int64("amt")
if cmd.IsSet("amt") && amount <= 0 {
return nil, errors.New("amount must be positive")
}
if amount != 0 && cmd.Bool("sweepall") {
return nil, errors.New("amount cannot be set if " +
"attempting to sweep all coins out of the wallet")
}
feeRateFlag, err := checkNotBothSet(
cmd, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
return nil, err
}
if _, err := checkNotBothSet(
cmd, feeRateFlag, "conf_target",
); err != nil {
return nil, err
}
var satPerByte int64
if cmd.IsSet("sat_per_byte") {
satPerByte = cmd.Int64("sat_per_byte")
if satPerByte < 0 {
return nil, fmt.Errorf("sat_per_byte must be " +
"non-negative")
}
}
confTarget := cmd.Int64("conf_target")
if confTarget < 0 {
return nil, fmt.Errorf("conf_target must be non-negative")
}
if confTarget > math.MaxInt32 {
return nil, fmt.Errorf("conf_target exceeds maximum " +
"int32 value")
}
minConfs := cmd.Uint64("min_confs")
if minConfs > math.MaxInt32 {
return nil, fmt.Errorf("min_confs exceeds maximum " +
"int32 value")
}
var outpoints []*lnrpc.OutPoint
utxos := cmd.StringSlice("utxo")
if len(utxos) > 0 {
outpoints, err = lnd.UtxosToOutpoints(utxos)
if err != nil {
return nil, fmt.Errorf("unable to decode utxos: %w", err)
}
}
coinSelectionStrategy, err := parseStaticAddressCoinSelectionStrategy(cmd)
if err != nil {
return nil, err
}
return &looprpc.NewStaticAddressRequest{
SendCoinsRequest: &lnrpc.SendCoinsRequest{
Addr: addr,
Amount: amount,
TargetConf: int32(confTarget),
SatPerVbyte: cmd.Uint64("sat_per_vbyte"),
SatPerByte: satPerByte,
SendAll: cmd.Bool("sweepall"),
Label: cmd.String(
staticAddressFundingLabelFlag.Name,
),
MinConfs: int32(minConfs),
SpendUnconfirmed: minConfs == 0,
CoinSelectionStrategy: coinSelectionStrategy,
Outpoints: outpoints,
},
}, nil
}
func parseStaticAddressCoinSelectionStrategy(cmd *cli.Command) (
lnrpc.CoinSelectionStrategy, error) {
if !cmd.IsSet(staticAddressCoinSelectionStrategyFlag.Name) {
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
}
switch strategy := cmd.String(
staticAddressCoinSelectionStrategyFlag.Name); strategy {
case "global-config":
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
case "largest":
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
case "random":
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
default:
return 0, fmt.Errorf("unknown coin selection strategy %v",
strategy)
}
}
func confirmStaticAddressDeposit(req *looprpc.NewStaticAddressRequest,
addr string) bool {
sendCoinsReq := req.GetSendCoinsRequest()
if sendCoinsReq.GetSendAll() {
fmt.Println("Amount: sweep all eligible wallet funds")
} else {
fmt.Printf("Amount: %d\n", sendCoinsReq.GetAmount())
}
fmt.Printf("Destination address: %s\n", addr)
fmt.Printf("Confirm funding transaction (yes/no): ")
var answer string
fmt.Scanln(&answer)
return answer == "yes" || answer == "y"
}
var listUnspentCommand = &cli.Command{
Name: "listunspent",
Aliases: []string{"l"},
@ -846,6 +1115,24 @@ func lowConfDepositWarning(allDeposits []*looprpc.Deposit,
)
}
func maybeDisplayNewAddressWarning(ctx context.Context,
client looprpc.SwapClientClient) error {
_, err := client.GetStaticAddressSummary(
ctx, &looprpc.StaticAddressSummaryRequest{},
)
switch {
case err == nil:
return nil
case strings.Contains(err.Error(), address.ErrNoStaticAddress.Error()):
return displayNewAddressWarning()
default:
return err
}
}
func displayNewAddressWarning() error {
fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " +
".loop under your home directory will take your ability to " +

View file

@ -1,6 +1,8 @@
package main
import (
"context"
"errors"
"strings"
"testing"
@ -8,11 +10,65 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"google.golang.org/grpc"
)
type staticAddressSummaryErrorClient struct {
looprpc.SwapClientClient
err error
}
func (c *staticAddressSummaryErrorClient) GetStaticAddressSummary(
context.Context, *looprpc.StaticAddressSummaryRequest,
...grpc.CallOption) (*looprpc.StaticAddressSummaryResponse, error) {
return nil, c.err
}
func TestMaybeDisplayNewAddressWarningReturnsUnexpectedError(t *testing.T) {
t.Parallel()
expectedErr := errors.New("permission denied")
err := maybeDisplayNewAddressWarning(
context.Background(), &staticAddressSummaryErrorClient{
err: expectedErr,
},
)
require.ErrorIs(t, err, expectedErr)
}
func TestStaticAddressDepositRequestAllowsNoUtxos(t *testing.T) {
t.Parallel()
var req *looprpc.NewStaticAddressRequest
cmd := &cli.Command{
Name: "deposit",
Flags: depositStaticAddressCommand.Flags,
Action: func(_ context.Context, cmd *cli.Command) error {
var err error
req, err = staticAddressDepositRequest(
cmd, "bcrt1ptestaddress",
)
return err
},
}
err := cmd.Run(context.Background(), []string{
"deposit", "--amt", "1000000",
})
require.NoError(t, err)
require.Equal(t, "bcrt1ptestaddress", req.GetSendCoinsRequest().Addr)
require.EqualValues(t, 1_000_000, req.GetSendCoinsRequest().Amount)
require.Empty(t, req.GetSendCoinsRequest().Outpoints)
}
// TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the
// conservative warning threshold are included in the warning text.
func TestLowConfDepositWarningConfirmedOnly(t *testing.T) {
@ -196,6 +252,9 @@ func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
OutPoint: outpoint,
Value: btcutil.Amount(fixture.value),
ConfirmationHeight: fixture.confirmationHeight,
AddressParams: &address.Parameters{
Expiry: csvExpiry,
},
})
}
@ -204,8 +263,7 @@ func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
)
loopInSelected, err := loopin.SelectDeposits(
btcutil.Amount(targetAmount), loopInDeposits, csvExpiry,
blockHeight,
btcutil.Amount(targetAmount), loopInDeposits, blockHeight,
)
require.NoError(t, err)

View file

@ -65,6 +65,7 @@
" \"id\": \"bb7f050df0b7c3e1fe61010e10ad45e30ddf7acd301fa6e05a2ddb825b5c2efb\",\n",
" \"outpoint\": \"56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -92,6 +92,7 @@
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -101,6 +102,7 @@
" \"id\": \"86b5e2cdf9694c8e7398e42afde109766d7cd2142203905ba63fbd0eb1370ef3\",\n",
" \"outpoint\": \"bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -110,6 +112,7 @@
" \"id\": \"6c290f7536ea5097946afffac6a69906a26d775823ebbacedfe6f2d69c0745e4\",\n",
" \"outpoint\": \"5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -119,6 +122,7 @@
" \"id\": \"0182b4d895b1c467290ae7b5c6c42ff76b2a4225807a94211c973170d5a883eb\",\n",
" \"outpoint\": \"7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -65,6 +65,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPED_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -110,6 +110,7 @@
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -119,6 +120,7 @@
" \"id\": \"86b5e2cdf9694c8e7398e42afde109766d7cd2142203905ba63fbd0eb1370ef3\",\n",
" \"outpoint\": \"bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -128,6 +130,7 @@
" \"id\": \"6c290f7536ea5097946afffac6a69906a26d775823ebbacedfe6f2d69c0745e4\",\n",
" \"outpoint\": \"5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -137,6 +140,7 @@
" \"id\": \"0182b4d895b1c467290ae7b5c6c42ff76b2a4225807a94211c973170d5a883eb\",\n",
" \"outpoint\": \"7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -146,6 +150,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPED_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -155,6 +160,7 @@
" \"id\": \"bb7f050df0b7c3e1fe61010e10ad45e30ddf7acd301fa6e05a2ddb825b5c2efb\",\n",
" \"outpoint\": \"56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -77,6 +77,7 @@
" \"id\": \"7a7cbe9b90f23d47aa92eb10a9d323f7ace6e9eaab5b77379c63422c15da19c8\",\n",
" \"outpoint\": \"0e70673c1da3343648c26f779555346f30d235314838b1160826d0d5c29b4fba:1\",\n",
" \"state\": \"CHANNEL_PUBLISHED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
@ -86,6 +87,7 @@
" \"id\": \"ff9a43b2082f906a2e2758934220c4ce32393eb2823b292517ae081e16daded9\",\n",
" \"outpoint\": \"d2d6e50f157f0d31b8688a4af4f064edf3454714e92369b2c8c4d82477edbaca:0\",\n",
" \"state\": \"CHANNEL_PUBLISHED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"1000000\"\n",
" }\n",

View file

@ -40,7 +40,8 @@
"event": "request",
"message_type": "looprpc.NewStaticAddressRequest",
"payload": {
"client_key": ""
"client_key": "",
"send_coins_request": null
}
}
},
@ -64,7 +65,8 @@
"lines": [
"{\n",
" \"address\": \"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw\",\n",
" \"expiry\": 14400\n",
" \"expiry\": 14400,\n",
" \"send_coins_response\": null\n",
"}\n"
]
}

View file

@ -25,6 +25,7 @@
"\n",
"COMMANDS:\n",
" new, n Create a new static loop in address.\n",
" deposit Create and fund a new static loop in address.\n",
" listunspent, l List unspent static address outputs.\n",
" listdeposits Displays static address deposits. A filter can be applied to only show deposits in a specific state.\n",
" listwithdrawals Display a summary of past withdrawals.\n",

View file

@ -61,6 +61,7 @@
" \"id\": \"ea6abbf0571c0ba82117ae9f2086614eacea8b2913dc0544b70c00de78353e71\",\n",
" \"outpoint\": \"188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0\",\n",
" \"state\": \"DEPOSITED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"2500000\"\n",
" }\n",

View file

@ -228,6 +228,7 @@
" \"id\": \"ea6abbf0571c0ba82117ae9f2086614eacea8b2913dc0544b70c00de78353e71\",\n",
" \"outpoint\": \"188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"2500000\"\n",
" }\n",

View file

@ -209,6 +209,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -244,6 +244,7 @@
" \"id\": \"82771323e95dca403d966f70a88be39ef0a475ef6aa78694044ba9b87304ac63\",\n",
" \"outpoint\": \"da52bf383c4fe5c684221c311fc5756ccaee211b6c6e6f5ccc159622a6039271:1\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -235,6 +235,7 @@
" \"id\": \"d8a58536d8472873b9e2e1657468328360fda0b94231cfc5e29900cab735da84\",\n",
" \"outpoint\": \"f2280f0f086273be73bde92fd9b982208338a5ecebbe93b83b00c77c4d2f8d1b:0\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"550000\"\n",
" }\n",

View file

@ -159,6 +159,7 @@
" \"id\": \"ea6abbf0571c0ba82117ae9f2086614eacea8b2913dc0544b70c00de78353e71\",\n",
" \"outpoint\": \"188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"2500000\"\n",
" }\n",

View file

@ -214,6 +214,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPING_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -73,6 +73,7 @@
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",

View file

@ -427,6 +427,39 @@ Create a new static loop in address.
.PP
\fB--help, -h\fP: show help
.SS deposit
Create and fund a new static loop in address.
.PP
\fB--amt\fP="": the number of bitcoin denominated in satoshis to send to the new static address (default: 0)
.PP
\fB--coin_selection_strategy\fP="": (optional) the strategy to use for selecting coins. Possible values are 'largest', 'random', or 'global-config'. If either 'largest' or 'random' is specified, it will override the globally configured strategy in lnd.conf (default: global-config)
.PP
\fB--conf_target\fP="": (optional) the number of blocks that the funding transaction should confirm in, will be used for fee estimation (default: 0)
.PP
\fB--force\fP: if set, the funding transaction will be broadcast without asking for confirmation
.PP
\fB--help, -h\fP: show help
.PP
\fB--label\fP="": (optional) a label for the funding transaction
.PP
\fB--min_confs\fP="": (optional) the minimum number of confirmations each one of your outputs used for the funding transaction must satisfy (default: 1)
.PP
\fB--sat_per_vbyte\fP="": (optional) a manual fee expressed in sat/vbyte that should be used when crafting the funding transaction (default: 0)
.PP
\fB--sweepall\fP: if set, then the amount field should be unset. This indicates that the wallet will attempt to sweep all outputs within the wallet or all funds in selected utxos (when supplied) to the new static address
.PP
\fB--utxo\fP="": a utxo specified as outpoint(tx:idx) which will be used as input for the funding transaction. This flag can be repeatedly used to specify multiple utxos as inputs. The selected utxos can either be entirely spent by specifying the sweepall flag or a specified amount can be spent in the utxos through the amt flag (default: [])
.SS listunspent, l
List unspent static address outputs.

View file

@ -532,7 +532,7 @@ The following flags are supported:
Create a new static loop in address.
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.
Creates a new static loop in address. On a fresh installation loopd initializes the static-address generation during startup. 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.
Usage:
@ -546,6 +546,33 @@ The following flags are supported:
|-----------------|-------------|------|:-------------:|
| `--help` (`-h`) | show help | bool | `false` |
### `static deposit` subcommand
Create and fund a new static loop in address.
Creates a new static loop in address and initiates a deposit by calling lnd's SendCoins API with the newly created address as the destination.
Usage:
```bash
$ loop [GLOBAL FLAGS] static deposit [COMMAND FLAGS] [ARGUMENTS...]
```
The following flags are supported:
| Name | Description | Type | Default value |
|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|:---------------:|
| `--amt="…"` | the number of bitcoin denominated in satoshis to send to the new static address | int | `0` |
| `--sweepall` | if set, then the amount field should be unset. This indicates that the wallet will attempt to sweep all outputs within the wallet or all funds in selected utxos (when supplied) to the new static address | bool | `false` |
| `--conf_target="…"` | (optional) the number of blocks that the funding transaction should confirm in, will be used for fee estimation | int | `0` |
| `--sat_per_vbyte="…"` | (optional) a manual fee expressed in sat/vbyte that should be used when crafting the funding transaction | uint | `0` |
| `--min_confs="…"` | (optional) the minimum number of confirmations each one of your outputs used for the funding transaction must satisfy | uint | `1` |
| `--force` | if set, the funding transaction will be broadcast without asking for confirmation | bool | `false` |
| `--coin_selection_strategy="…"` | (optional) the strategy to use for selecting coins. Possible values are 'largest', 'random', or 'global-config'. If either 'largest' or 'random' is specified, it will override the globally configured strategy in lnd.conf | string | `global-config` |
| `--utxo="…"` | a utxo specified as outpoint(tx:idx) which will be used as input for the funding transaction. This flag can be repeatedly used to specify multiple utxos as inputs. The selected utxos can either be entirely spent by specifying the sweepall flag or a specified amount can be spent in the utxos through the amt flag | string | `[]` |
| `--label="…"` | (optional) a label for the funding transaction | string |
| `--help` (`-h`) | show help | bool | `false` |
### `static listunspent` subcommand (aliases: `l`)
List unspent static address outputs.

View file

@ -2,6 +2,12 @@
#### New Features
* Static Address now derives fresh receive and change addresses while retaining
per-deposit address ownership across restarts for discovery, recovery, and
signing. The new `loop static deposit` command can create and fund an address
directly from the lnd wallet, and deposit listings identify the receiving
address. [PR #1139](https://github.com/lightninglabs/loop/pull/1139)
#### Breaking Changes
* Instant Out and reservation RPCs now require the `loop:out` permission.
@ -9,6 +15,12 @@
`ListReservations`, `InstantOut`, `InstantOutQuote`, or `ListInstantOuts`.
[PR #1194](https://github.com/lightninglabs/loop/pull/1194)
* Calling `NewStaticAddress` without `send_coins_request.addr` now derives and
returns a fresh receive address instead of reusing the address associated
with the client's L402. Integrations must not assume that repeated calls are
idempotent or return the same address.
[PR #1139](https://github.com/lightninglabs/loop/pull/1139)
#### Bug Fixes
* Loop Out requests now account for channel reserves when checking outbound

2
go.mod
View file

@ -182,7 +182,7 @@ require (
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.44.0 // indirect

View file

@ -38,6 +38,8 @@ import (
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/taproot-assets/rfqmath"
lndlabels "github.com/lightningnetwork/lnd/labels"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/queue"
@ -45,6 +47,7 @@ import (
"github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
const (
@ -1105,24 +1108,13 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
"deposits: %w", err)
}
// TODO(hieblmi): add params to deposit for multi-address
// support.
params, err := s.staticAddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, fmt.Errorf("unable to retrieve static "+
"address parameters: %w", err)
}
info, err := s.lnd.Client.GetInfo(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get lnd info: %w",
err)
}
selectedDeposits, err := loopin.SelectDeposits(
selectedAmount, deposits, params.Expiry,
info.BlockHeight,
selectedAmount, deposits, info.BlockHeight,
)
if err != nil {
return nil, fmt.Errorf("unable to select deposits: %w",
@ -1862,20 +1854,169 @@ func rpcInstantOut(instantOut *instantout.InstantOut) *looprpc.InstantOut {
// NewStaticAddress is the rpc endpoint for loop clients to request a new static
// address.
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
_ *looprpc.NewStaticAddressRequest) (
req *looprpc.NewStaticAddressRequest) (
*looprpc.NewStaticAddressResponse, error) {
sendCoinsReq := req.GetSendCoinsRequest()
if err := validateStaticAddressSendCoinsRequest(sendCoinsReq); err != nil {
return nil, err
}
if sendCoinsReq.GetAddr() != "" {
return s.fundExistingStaticAddress(ctx, sendCoinsReq)
}
staticAddress, expiry, err := s.staticAddressManager.NewAddress(ctx)
if err != nil {
return nil, err
}
sendCoinsResp, err := s.sendCoinsToStaticAddress(
ctx, staticAddress.String(), sendCoinsReq,
)
if err != nil {
return nil, fmt.Errorf("static address %s created, but "+
"funding transaction failed: %w", staticAddress, err)
}
return &looprpc.NewStaticAddressResponse{
Address: staticAddress.String(),
Expiry: uint32(expiry),
Address: staticAddress.String(),
Expiry: uint32(expiry),
SendCoinsResponse: sendCoinsResp,
}, nil
}
func (s *swapClientServer) fundExistingStaticAddress(ctx context.Context,
req *lnrpc.SendCoinsRequest) (*looprpc.NewStaticAddressResponse, error) {
staticAddress, expiry, err := s.staticAddressForDeposit(ctx, req.Addr)
if err != nil {
return nil, err
}
sendCoinsResp, err := s.sendCoinsToStaticAddress(
ctx, staticAddress, req,
)
if err != nil {
return nil, fmt.Errorf("static address %s funding transaction "+
"failed: %w", staticAddress, err)
}
return &looprpc.NewStaticAddressResponse{
Address: staticAddress,
Expiry: expiry,
SendCoinsResponse: sendCoinsResp,
}, nil
}
func (s *swapClientServer) staticAddressForDeposit(ctx context.Context,
addr string) (string, uint32, error) {
addresses, err := s.staticAddressManager.GetAllAddresses(ctx)
if err != nil {
return "", 0, err
}
for _, params := range addresses {
staticAddress, err := s.staticAddressManager.GetTaprootAddress(
params.ClientPubkey, params.ServerPubkey,
int64(params.Expiry),
)
if err != nil {
return "", 0, err
}
if staticAddress.String() == addr {
return addr, params.Expiry, nil
}
}
return "", 0, status.Errorf(codes.InvalidArgument,
"send_coins_request.addr is not a known static address")
}
func validateStaticAddressSendCoinsRequest(req *lnrpc.SendCoinsRequest) error {
if req == nil {
return nil
}
switch {
case req.Amount < 0:
return status.Error(codes.InvalidArgument, "send_coins_request."+
"amount must be non-negative")
case req.Amount == 0 && !req.SendAll:
return status.Error(codes.InvalidArgument, "send_coins_request "+
"must set amount or send_all")
case req.Amount != 0 && req.SendAll:
return status.Error(codes.InvalidArgument, "send_coins_request."+
"amount cannot be set when send_all is true")
case req.TargetConf < 0:
return status.Error(codes.InvalidArgument, "send_coins_request."+
"target_conf must be non-negative")
case req.SatPerByte < 0: //nolint:staticcheck
return status.Error(codes.InvalidArgument, "send_coins_request."+
"sat_per_byte must be non-negative")
case req.TargetConf != 0 &&
(req.SatPerVbyte != 0 || req.SatPerByte != 0): //nolint:staticcheck
return status.Error(codes.InvalidArgument, "send_coins_request "+
"can set either target_conf or a fee rate, but not both")
case req.SatPerVbyte != 0 && req.SatPerByte != 0: //nolint:staticcheck
return status.Error(codes.InvalidArgument, "send_coins_request "+
"can set either sat_per_vbyte or sat_per_byte, but not "+
"both")
case req.MinConfs < 0:
return status.Error(codes.InvalidArgument, "send_coins_request."+
"min_confs must be non-negative")
}
if _, err := lnrpc.ExtractMinConfs(
req.MinConfs, req.SpendUnconfirmed,
); err != nil {
return status.Errorf(codes.InvalidArgument, "send_coins_request "+
"min_confs/spend_unconfirmed invalid: %v", err)
}
if _, err := lndlabels.ValidateAPI(req.Label); err != nil {
return status.Errorf(codes.InvalidArgument, "send_coins_request "+
"label invalid: %v", err)
}
if _, err := lnrpc.UnmarshallCoinSelectionStrategy(
req.CoinSelectionStrategy, nil,
); err != nil {
return status.Errorf(codes.InvalidArgument, "send_coins_request "+
"coin_selection_strategy invalid: %v", err)
}
return nil
}
func (s *swapClientServer) sendCoinsToStaticAddress(ctx context.Context,
addr string, req *lnrpc.SendCoinsRequest) (*lnrpc.SendCoinsResponse,
error) {
if req == nil {
return nil, nil
}
sendCoinsReq := proto.Clone(req).(*lnrpc.SendCoinsRequest)
sendCoinsReq.Addr = addr
rawCtx, timeout, rawClient := s.lnd.Client.RawClientWithMacAuth(ctx)
rawCtx, cancel := context.WithTimeout(rawCtx, timeout)
defer cancel()
return rawClient.SendCoins(rawCtx, sendCoinsReq)
}
// ListUnspentDeposits returns a list of utxos behind the static address.
func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
req *looprpc.ListUnspentDepositsRequest) (
@ -1883,7 +2024,7 @@ func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
// List all unspent utxos the wallet sees, regardless of the number of
// confirmations.
staticAddress, utxos, err := s.staticAddressManager.ListUnspentRaw(
utxos, err := s.staticAddressManager.ListUnspentRaw(
ctx, req.MinConfs, req.MaxConfs,
)
if err != nil {
@ -1934,6 +2075,20 @@ func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
continue
}
params := s.staticAddressManager.GetParameters(u.PkScript)
if params == nil {
return nil, fmt.Errorf("missing static address "+
"parameters for %v", u.OutPoint)
}
staticAddress, err := s.staticAddressManager.GetTaprootAddress(
params.ClientPubkey, params.ServerPubkey,
int64(params.Expiry),
)
if err != nil {
return nil, err
}
utxo := &looprpc.Utxo{
StaticAddress: staticAddress.String(),
AmountSat: int64(u.Value),
@ -2047,7 +2202,10 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
f := func(d *deposit.Deposit) bool {
return slices.Contains(outpoints, d.OutPoint.String())
}
filteredDeposits = filter(allDeposits, f)
filteredDeposits, err = s.filterDeposits(allDeposits, f)
if err != nil {
return nil, err
}
if len(outpoints) != len(filteredDeposits) {
return nil, fmt.Errorf("not all outpoints found in " +
@ -2063,11 +2221,14 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
return d.IsInState(toServerState(req.StateFilter))
}
filteredDeposits = filter(allDeposits, f)
filteredDeposits, err = s.filterDeposits(allDeposits, f)
if err != nil {
return nil, err
}
}
// Calculate the blocks until expiry for each deposit.
err = s.populateBlocksUntilExpiry(ctx, filteredDeposits)
err = s.populateBlocksUntilExpiry(ctx, allDeposits, filteredDeposits)
if err != nil {
infof("Failed to populate blocks until expiry: %v", err)
}
@ -2096,26 +2257,11 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
)
for _, w := range withdrawals {
deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
for _, d := range w.Deposits {
deposits = append(deposits, &looprpc.Deposit{
Id: d.ID[:],
Outpoint: d.OutPoint.String(),
Value: int64(d.Value),
ConfirmationHeight: d.GetConfirmationHeight(),
State: toClientDepositState(
d.GetState(),
),
})
}
withdrawal := &looprpc.StaticAddressWithdrawal{
TxId: w.TxID.String(),
Deposits: deposits,
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
ChangeAmountSatoshis: int64(w.ChangeAmount),
ConfirmationHeight: uint32(w.ConfirmationHeight),
withdrawal, err := s.rpcStaticAddressWithdrawal(w)
if err != nil {
return nil, err
}
clientWithdrawals = append(clientWithdrawals, withdrawal)
}
@ -2124,6 +2270,29 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
}, nil
}
func (s *swapClientServer) rpcStaticAddressWithdrawal(
w withdraw.Withdrawal) (*looprpc.StaticAddressWithdrawal, error) {
deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
for _, d := range w.Deposits {
rpcDeposit, err := s.rpcDeposit(d)
if err != nil {
return nil, err
}
deposits = append(deposits, rpcDeposit)
}
return &looprpc.StaticAddressWithdrawal{
TxId: w.TxID.String(),
Deposits: deposits,
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
ChangeAmountSatoshis: int64(w.ChangeAmount),
ConfirmationHeight: uint32(w.ConfirmationHeight),
}, nil
}
// ListStaticAddressSwaps returns a list of all swaps that are currently pending
// or previously succeeded.
func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
@ -2145,13 +2314,6 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
return nil, err
}
addrParams, err := s.staticAddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, err
}
// Fetch all deposits at once and index them by swap hash for a quick
// lookup.
allDeposits, err := s.depositManager.GetAllDeposits(ctx)
@ -2192,22 +2354,23 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
if ds, ok := depositsBySwap[swp.SwapHash]; ok {
protoDeposits = make([]*looprpc.Deposit, 0, len(ds))
for _, d := range ds {
state := toClientDepositState(d.GetState())
confirmationHeight := d.GetConfirmationHeight()
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static "+
"address parameters for deposit %v",
d.OutPoint)
}
blocksUntilExpiry := depositBlocksUntilExpiry(
confirmationHeight, addrParams.Expiry,
confirmationHeight,
d.AddressParams.Expiry,
int64(lndInfo.BlockHeight),
)
pd := &looprpc.Deposit{
Id: d.ID[:],
State: state,
Outpoint: d.OutPoint.String(),
Value: int64(d.Value),
ConfirmationHeight: confirmationHeight,
SwapHash: d.SwapHash[:],
BlocksUntilExpiry: blocksUntilExpiry,
pd, err := s.rpcDeposit(d)
if err != nil {
return nil, err
}
pd.BlocksUntilExpiry = blocksUntilExpiry
protoDeposits = append(protoDeposits, pd)
}
}
@ -2526,11 +2689,14 @@ func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
}
// Build a list of used deposits for the response.
usedDeposits := filter(
usedDeposits, err := s.filterDeposits(
loopIn.Deposits, func(d *deposit.Deposit) bool { return true },
)
if err != nil {
return nil, err
}
err = s.populateBlocksUntilExpiry(ctx, usedDeposits)
err = s.populateBlocksUntilExpiry(ctx, loopIn.Deposits, usedDeposits)
if err != nil {
infof("Failed to populate blocks until expiry: %v", err)
}
@ -2572,21 +2738,31 @@ func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
// Calculate the blocks until expiry for each deposit and return the modified
// StaticAddressLoopInResponse.
func (s *swapClientServer) populateBlocksUntilExpiry(ctx context.Context,
deposits []*looprpc.Deposit) error {
sourceDeposits []*deposit.Deposit, deposits []*looprpc.Deposit) error {
lndInfo, err := s.lnd.Client.GetInfo(ctx)
if err != nil {
return err
}
bestBlockHeight := int64(lndInfo.BlockHeight)
params, err := s.staticAddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return err
expiryByOutpoint := make(map[string]uint32, len(sourceDeposits))
for _, d := range sourceDeposits {
if d.AddressParams == nil {
continue
}
expiryByOutpoint[d.OutPoint.String()] = d.AddressParams.Expiry
}
bestBlockHeight := int64(lndInfo.BlockHeight)
for i := range len(deposits) {
expiry, ok := expiryByOutpoint[deposits[i].Outpoint]
if !ok {
continue
}
deposits[i].BlocksUntilExpiry = depositBlocksUntilExpiry(
deposits[i].ConfirmationHeight, params.Expiry,
deposits[i].ConfirmationHeight, expiry,
bestBlockHeight,
)
}
@ -2635,35 +2811,65 @@ func (s *swapClientServer) StaticOpenChannel(ctx context.Context,
type filterFunc func(deposits *deposit.Deposit) bool
func filter(deposits []*deposit.Deposit, f filterFunc) []*looprpc.Deposit {
func (s *swapClientServer) filterDeposits(deposits []*deposit.Deposit,
f filterFunc) ([]*looprpc.Deposit, error) {
var clientDeposits []*looprpc.Deposit
for _, d := range deposits {
if !f(d) {
continue
}
swapHash := make([]byte, 0, len(lntypes.Hash{}))
if d.SwapHash != nil {
swapHash = d.SwapHash[:]
}
hash := d.Hash
outpoint := wire.NewOutPoint(&hash, d.Index).String()
deposit := &looprpc.Deposit{
Id: d.ID[:],
State: toClientDepositState(
d.GetState(),
),
Outpoint: outpoint,
Value: int64(d.Value),
ConfirmationHeight: d.GetConfirmationHeight(),
SwapHash: swapHash,
deposit, err := s.rpcDeposit(d)
if err != nil {
return nil, err
}
clientDeposits = append(clientDeposits, deposit)
}
return clientDeposits
return clientDeposits, nil
}
func (s *swapClientServer) rpcDeposit(d *deposit.Deposit) (
*looprpc.Deposit, error) {
swapHash := make([]byte, 0, len(lntypes.Hash{}))
if d.SwapHash != nil {
swapHash = d.SwapHash[:]
}
hash := d.Hash
outpoint := wire.NewOutPoint(&hash, d.Index).String()
deposit := &looprpc.Deposit{
Id: d.ID[:],
State: toClientDepositState(
d.GetState(),
),
Outpoint: outpoint,
Value: int64(d.Value),
ConfirmationHeight: d.GetConfirmationHeight(),
SwapHash: swapHash,
}
if d.AddressParams == nil {
return deposit, nil
}
if s.staticAddressManager == nil {
return nil, fmt.Errorf("static address manager not configured")
}
staticAddress, err := s.staticAddressManager.GetTaprootAddress(
d.AddressParams.ClientPubkey, d.AddressParams.ServerPubkey,
int64(d.AddressParams.Expiry),
)
if err != nil {
return nil, err
}
deposit.StaticAddress = staticAddress.String()
return deposit, nil
}
func toClientDepositState(state fsm.StateType) looprpc.DepositState {

View file

@ -2,6 +2,7 @@ package loopd
import (
"context"
"strings"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
@ -13,7 +14,9 @@ import (
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/withdraw"
mock_lnd "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)
@ -62,12 +65,44 @@ func (s *staticAddrDepositStore) AllDeposits(context.Context) (
return s.allDeposits, nil
}
type staticAddrTestAddressManager struct{}
type staticAddrTestAddressManager struct {
params *address.Parameters
}
func newStaticAddrTestAddressManager() *staticAddrTestAddressManager {
_, client := mock_lnd.CreateKey(1)
_, server := mock_lnd.CreateKey(2)
return &staticAddrTestAddressManager{
params: &address.Parameters{
ID: 1,
ClientPubkey: client,
ServerPubkey: server,
Expiry: 10,
PkScript: []byte("pkscript"),
},
}
}
func (s *staticAddrTestAddressManager) GetStaticAddressParameters(
context.Context) (*script.Parameters, error) {
return nil, nil
return s.params, nil
}
func (s *staticAddrTestAddressManager) GetStaticAddressID(
context.Context, []byte) (int32, error) {
return s.params.ID, nil
}
func (s *staticAddrTestAddressManager) GetParameters(
pkScript []byte) *address.Parameters {
params := *s.params
params.PkScript = pkScript
return &params
}
func (s *staticAddrTestAddressManager) GetStaticAddress(
@ -99,7 +134,7 @@ func newTestDepositManager(
}
return deposit.NewManager(&deposit.ManagerConfig{
AddressManager: &staticAddrTestAddressManager{},
AddressManager: newStaticAddrTestAddressManager(),
Store: &staticAddrDepositStore{
allDeposits: deposits,
byOutpoint: byOutpoint,
@ -136,6 +171,161 @@ func newTestStaticAddressContext(t *testing.T) (*address.Manager,
return addrMgr, mock
}
func TestValidateStaticAddressSendCoinsRequest(t *testing.T) {
t.Parallel()
tests := []struct {
name string
req *lnrpc.SendCoinsRequest
err string
}{
{
name: "nil",
},
{
name: "amount",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
},
},
{
name: "send all",
req: &lnrpc.SendCoinsRequest{
SendAll: true,
},
},
{
name: "existing addr",
req: &lnrpc.SendCoinsRequest{
Addr: "bcrt1ptestaddress",
Amount: 10_000,
},
},
{
name: "missing amount",
req: &lnrpc.SendCoinsRequest{},
err: "must set amount or send_all",
},
{
name: "negative amount",
req: &lnrpc.SendCoinsRequest{
Amount: -1,
},
err: "amount must be non-negative",
},
{
name: "amount and send all",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
SendAll: true,
},
err: "amount cannot be set when send_all is true",
},
{
name: "target and fee rate",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
TargetConf: 6,
SatPerVbyte: 1,
SatPerByte: 0,
SendAll: false,
MinConfs: 1,
Outpoints: nil,
SpendUnconfirmed: false,
},
err: "can set either target_conf or a fee rate",
},
{
name: "both fee rates",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
SatPerVbyte: 1,
SatPerByte: 1,
},
err: "can set either sat_per_vbyte or sat_per_byte",
},
{
name: "negative min confs",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
MinConfs: -1,
},
err: "min_confs must be non-negative",
},
{
name: "min confs with spend unconfirmed",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
MinConfs: 1,
SpendUnconfirmed: true,
},
err: "spend_unconfirmed invalid",
},
{
name: "invalid label",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
Label: strings.Repeat("x", 501),
},
err: "label invalid",
},
{
name: "invalid coin selection strategy",
req: &lnrpc.SendCoinsRequest{
Amount: 10_000,
CoinSelectionStrategy: lnrpc.CoinSelectionStrategy(99),
},
err: "coin_selection_strategy invalid",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
err := validateStaticAddressSendCoinsRequest(test.req)
if test.err == "" {
require.NoError(t, err)
return
}
require.ErrorContains(t, err, test.err)
})
}
}
func TestStaticAddressForDeposit(t *testing.T) {
t.Parallel()
ctx := context.Background()
addrMgr, _ := newTestStaticAddressContext(t)
server := &swapClientServer{
staticAddressManager: addrMgr,
}
addresses, err := addrMgr.GetAllAddresses(ctx)
require.NoError(t, err)
require.Len(t, addresses, 1)
expectedAddr, err := addrMgr.GetTaprootAddress(
addresses[0].ClientPubkey, addresses[0].ServerPubkey,
int64(addresses[0].Expiry),
)
require.NoError(t, err)
addr, expiry, err := server.staticAddressForDeposit(
ctx, expectedAddr.String(),
)
require.NoError(t, err)
require.Equal(t, expectedAddr.String(), addr)
require.Equal(t, addresses[0].Expiry, expiry)
_, _, err = server.staticAddressForDeposit(
ctx, "bcrt1punknownstaticaddress",
)
require.ErrorContains(t, err, "not a known static address")
}
// TestListStaticAddressDepositsReturnsVisibleDeposits verifies normal deposit
// listings include visible deposit records.
func TestListStaticAddressDepositsReturnsVisibleDeposits(t *testing.T) {
@ -150,6 +340,17 @@ func TestListStaticAddressDepositsReturnsVisibleDeposits(t *testing.T) {
available.SetState(deposit.Deposited)
addrMgr, lnd := newTestStaticAddressContext(t)
addresses, err := addrMgr.GetAllAddresses(context.Background())
require.NoError(t, err)
require.Len(t, addresses, 1)
available.AddressParams = addresses[0]
expectedAddr, err := addrMgr.GetTaprootAddress(
addresses[0].ClientPubkey, addresses[0].ServerPubkey,
int64(addresses[0].Expiry),
)
require.NoError(t, err)
server := &swapClientServer{
depositManager: newTestDepositManager(available),
staticAddressManager: addrMgr,
@ -165,6 +366,52 @@ func TestListStaticAddressDepositsReturnsVisibleDeposits(t *testing.T) {
t, available.OutPoint.String(),
resp.FilteredDeposits[0].Outpoint,
)
require.Equal(
t, expectedAddr.String(),
resp.FilteredDeposits[0].StaticAddress,
)
}
// TestStaticAddressWithdrawalIncludesDepositAddress verifies withdrawal
// listings use the common deposit conversion path, including the address that
// received each deposit.
func TestStaticAddressWithdrawalIncludesDepositAddress(t *testing.T) {
t.Parallel()
addrMgr, _ := newTestStaticAddressContext(t)
addresses, err := addrMgr.GetAllAddresses(context.Background())
require.NoError(t, err)
require.Len(t, addresses, 1)
expectedAddr, err := addrMgr.GetTaprootAddress(
addresses[0].ClientPubkey, addresses[0].ServerPubkey,
int64(addresses[0].Expiry),
)
require.NoError(t, err)
d := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{3},
Index: 3,
},
AddressParams: addresses[0],
}
d.SetState(deposit.Withdrawn)
server := &swapClientServer{
staticAddressManager: addrMgr,
}
rpcWithdrawal, err := server.rpcStaticAddressWithdrawal(
withdraw.Withdrawal{
Deposits: []*deposit.Deposit{d},
},
)
require.NoError(t, err)
require.Len(t, rpcWithdrawal.Deposits, 1)
require.Equal(
t, expectedAddr.String(),
rpcWithdrawal.Deposits[0].StaticAddress,
)
}
// TestGetStaticAddressSummaryTotalsDeposits verifies visible deposits are

View file

@ -1,7 +1,9 @@
package loopd
import (
"bytes"
"context"
"database/sql"
"fmt"
"os"
"testing"
@ -414,6 +416,17 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) {
}
testDeposit.SetState(deposit.LoopedIn)
_, clientPubkey := mock_lnd.CreateKey(1)
_, serverPubkey := mock_lnd.CreateKey(2)
staticAddressParams := &script.Parameters{
ID: 1,
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
Expiry: staticAddressExpiry,
PkScript: []byte("pkscript"),
}
testDeposit.AddressParams = staticAddressParams
initiationTime := time.Unix(1_234, 567).UTC()
lastUpdateTime := time.Unix(2_345, 678).UTC()
staticLoopIn := &loopin.StaticAddressLoopIn{
@ -444,15 +457,8 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) {
}, 1)
require.NoError(t, err)
_, clientPubkey := mock_lnd.CreateKey(1)
_, serverPubkey := mock_lnd.CreateKey(2)
addrStore := &mockAddressStore{
params: []*script.Parameters{{
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
Expiry: staticAddressExpiry,
PkScript: []byte("pkscript"),
}},
params: []*script.Parameters{staticAddressParams},
}
addrMgr, err := address.NewManager(&address.ManagerConfig{
Store: addrStore,
@ -1883,10 +1889,25 @@ type mockAddressStore struct {
func (s *mockAddressStore) CreateStaticAddress(_ context.Context,
p *script.Parameters) error {
if p.ID == 0 {
p.ID = int32(len(s.params) + 1)
}
s.params = append(s.params, p)
return nil
}
func (s *mockAddressStore) GetStaticAddressID(_ context.Context,
pkScript []byte) (int32, error) {
for _, p := range s.params {
if bytes.Equal(p.PkScript, pkScript) {
return p.ID, nil
}
}
return 0, sql.ErrNoRows
}
func (s *mockAddressStore) GetStaticAddress(_ context.Context, _ []byte) (
*script.Parameters, error) {
@ -1903,6 +1924,16 @@ func (s *mockAddressStore) GetAllStaticAddresses(_ context.Context) (
return s.params, nil
}
func (s *mockAddressStore) GetLegacyParameters(_ context.Context) (
*address.Parameters, error) {
if len(s.params) == 0 {
return nil, sql.ErrNoRows
}
return s.params[0], nil
}
// mockDepositStore implements deposit.Store minimally for DepositsForOutpoints.
type mockDepositStore struct {
byOutpoint map[string]*deposit.Deposit
@ -2038,7 +2069,12 @@ func TestListUnspentDeposits(t *testing.T) {
// Prepare a single static address parameter set.
_, client := mock_lnd.CreateKey(1)
_, server := mock_lnd.CreateKey(2)
pkScript := []byte("pkscript")
staticAddress, err := script.NewStaticAddress(
input.MuSig2Version100RC2, 10, client, server,
)
require.NoError(t, err)
pkScript, err := staticAddress.StaticAddressScript()
require.NoError(t, err)
addrParams := &script.Parameters{
ClientPubkey: client,
ServerPubkey: server,
@ -2056,6 +2092,8 @@ func TestListUnspentDeposits(t *testing.T) {
// ChainNotifier and AddressClient are not needed for this test.
}, 1)
require.NoError(t, err)
_, err = addrMgr.EnsureStaticAddressSeed(ctx)
require.NoError(t, err)
// Construct several UTXOs with different confirmation counts.
makeUtxo := func(idx uint32, confs int64) *lnwallet.Utxo {

View file

@ -0,0 +1 @@
ALTER TABLE deposits DROP COLUMN static_address_id;

View file

@ -0,0 +1,8 @@
ALTER TABLE deposits ADD static_address_id INT REFERENCES static_addresses(id);
UPDATE deposits
SET static_address_id = (
SELECT id FROM static_addresses ORDER BY id ASC LIMIT 1
)
WHERE static_address_id IS NULL
AND EXISTS (SELECT 1 FROM static_addresses);

View file

@ -0,0 +1 @@
ALTER TABLE static_address_swaps DROP COLUMN change_static_address_id;

View file

@ -0,0 +1,13 @@
ALTER TABLE static_address_swaps
ADD change_static_address_id INT REFERENCES static_addresses(id);
-- Existing fractional swaps sent change back to the legacy static address.
-- Backfill that relation so in-flight swaps remain recoverable after the
-- client starts requiring explicit per-swap change metadata.
UPDATE static_address_swaps
SET change_static_address_id = (
SELECT id FROM static_addresses ORDER BY id ASC LIMIT 1
)
WHERE selected_amount > 0
AND change_static_address_id IS NULL
AND EXISTS (SELECT 1 FROM static_addresses);

View file

@ -0,0 +1,8 @@
ALTER TABLE static_address_swaps
DROP COLUMN confirmed_htlc_output_value;
ALTER TABLE static_address_swaps
DROP COLUMN confirmed_htlc_output_index;
ALTER TABLE static_address_swaps
DROP COLUMN confirmed_htlc_tx_id;

View file

@ -0,0 +1,8 @@
ALTER TABLE static_address_swaps
ADD confirmed_htlc_tx_id TEXT;
ALTER TABLE static_address_swaps
ADD confirmed_htlc_output_index INTEGER;
ALTER TABLE static_address_swaps
ADD confirmed_htlc_output_value BIGINT;

View file

@ -20,6 +20,7 @@ type Deposit struct {
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
}
type DepositUpdate struct {
@ -151,6 +152,10 @@ type StaticAddressSwap struct {
Fast bool
ConfirmationRiskDecision string
ConfirmationRiskDecisionTime sql.NullTime
ChangeStaticAddressID sql.NullInt32
ConfirmedHtlcTxID sql.NullString
ConfirmedHtlcOutputIndex sql.NullInt32
ConfirmedHtlcOutputValue sql.NullInt64
}
type StaticAddressSwapUpdate struct {

View file

@ -10,7 +10,7 @@ import (
)
type Querier interface {
AllDeposits(ctx context.Context) ([]Deposit, error)
AllDeposits(ctx context.Context) ([]AllDepositsRow, error)
AllStaticAddresses(ctx context.Context) ([]StaticAddress, error)
CancelBatch(ctx context.Context, id int32) error
CreateDeposit(ctx context.Context, arg CreateDepositParams) error
@ -18,19 +18,20 @@ type Querier interface {
CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error
CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error
CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error
DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error)
DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (DepositForOutpointRow, error)
DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error)
DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error)
FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error)
GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error)
GetDeposit(ctx context.Context, depositID []byte) (Deposit, error)
GetDeposit(ctx context.Context, depositID []byte) (GetDepositRow, error)
GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error)
GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error)
GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error)
GetLastUpdateID(ctx context.Context, swapHash []byte) (int32, error)
GetLatestDepositUpdate(ctx context.Context, depositID []byte) (DepositUpdate, error)
GetLegacyAddress(ctx context.Context) (StaticAddress, error)
GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error)
GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([]StaticAddressSwapUpdate, error)
GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error)
@ -42,6 +43,7 @@ type Querier interface {
GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error)
GetReservations(ctx context.Context) ([]Reservation, error)
GetStaticAddress(ctx context.Context, pkscript []byte) (StaticAddress, error)
GetStaticAddressID(ctx context.Context, pkscript []byte) (int32, error)
GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error)
GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error)
GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error)
@ -68,6 +70,7 @@ type Querier interface {
OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error
OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
RecordStaticAddressRiskDecision(ctx context.Context, arg RecordStaticAddressRiskDecisionParams) error
SetAllNullDepositsStaticAddressID(ctx context.Context, staticAddressID sql.NullInt32) error
SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error)
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error

View file

@ -7,7 +7,8 @@ INSERT INTO deposits (
confirmation_height,
timeout_sweep_pk_script,
expiry_sweep_txid,
finalized_withdrawal_tx
finalized_withdrawal_tx,
static_address_id
) VALUES (
$1,
$2,
@ -16,7 +17,8 @@ INSERT INTO deposits (
$5,
$6,
$7,
$8
$8,
$9
);
-- name: UpdateDeposit :exec
@ -43,17 +45,35 @@ INSERT INTO deposit_updates (
-- name: GetDeposit :one
SELECT
*
d.*,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
WHERE
deposit_id = $1;
-- name: DepositForOutpoint :one
SELECT
*
d.*,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
WHERE
tx_hash = $1
AND
@ -61,11 +81,20 @@ AND
-- name: AllDeposits :many
SELECT
*
d.*,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
ORDER BY
id ASC;
d.id ASC;
-- name: GetLatestDepositUpdate :one
SELECT
@ -76,4 +105,9 @@ WHERE
deposit_id = $1
ORDER BY
update_timestamp DESC
LIMIT 1;
LIMIT 1;
-- name: SetAllNullDepositsStaticAddressID :exec
UPDATE deposits
SET static_address_id = $1
WHERE static_address_id IS NULL;

View file

@ -10,7 +10,8 @@ INSERT INTO static_address_swaps (
htlc_tx_fee_rate_sat_kw,
htlc_timeout_sweep_tx_id,
htlc_timeout_sweep_address,
fast
fast,
change_static_address_id
) VALUES (
$1,
$2,
@ -22,14 +23,18 @@ INSERT INTO static_address_swaps (
$8,
$9,
$10,
$11
$11,
$12
);
-- name: UpdateStaticAddressLoopIn :exec
UPDATE static_address_swaps
SET
htlc_tx_fee_rate_sat_kw = $2,
htlc_timeout_sweep_tx_id = $3
htlc_timeout_sweep_tx_id = $3,
confirmed_htlc_tx_id = $4,
confirmed_htlc_output_index = $5,
confirmed_htlc_output_value = $6
WHERE
swap_hash = $1;
@ -64,13 +69,24 @@ INSERT INTO static_address_swap_updates (
SELECT
swaps.*,
static_address_swaps.*,
htlc_keys.*
htlc_keys.*,
change_address.client_pubkey change_client_pubkey,
change_address.server_pubkey change_server_pubkey,
change_address.expiry change_expiry,
change_address.client_key_family change_client_key_family,
change_address.client_key_index change_client_key_index,
change_address.pkscript change_pkscript,
change_address.protocol_version change_protocol_version,
change_address.initiation_height change_initiation_height
FROM
swaps
JOIN
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
LEFT JOIN
static_addresses change_address
ON static_address_swaps.change_static_address_id = change_address.id
WHERE
swaps.swap_hash = $1;
@ -78,13 +94,24 @@ WHERE
SELECT
swaps.*,
static_address_swaps.*,
htlc_keys.*
htlc_keys.*,
change_address.client_pubkey change_client_pubkey,
change_address.server_pubkey change_server_pubkey,
change_address.expiry change_expiry,
change_address.client_key_family change_client_key_family,
change_address.client_key_index change_client_key_index,
change_address.pkscript change_pkscript,
change_address.protocol_version change_protocol_version,
change_address.initiation_height change_initiation_height
FROM
swaps
JOIN
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
LEFT JOIN
static_addresses change_address
ON static_address_swaps.change_static_address_id = change_address.id
JOIN
static_address_swap_updates u ON swaps.swap_hash = u.swap_hash
-- This subquery ensures that we are checking only the latest update for
@ -147,10 +174,19 @@ WHERE
-- name: DepositsForSwapHash :many
SELECT
d.*,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height,
u.update_state,
u.update_timestamp
FROM
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
LEFT JOIN
deposit_updates u ON u.id = (
SELECT id
@ -161,5 +197,3 @@ FROM
)
WHERE
d.swap_hash = $1;

View file

@ -1,10 +1,15 @@
-- name: AllStaticAddresses :many
SELECT * FROM static_addresses;
SELECT * FROM static_addresses
ORDER BY id ASC;
-- name: GetStaticAddress :one
SELECT * FROM static_addresses
WHERE pkscript=$1;
-- name: GetStaticAddressID :one
SELECT id FROM static_addresses
WHERE pkscript=$1;
-- name: CreateStaticAddress :exec
INSERT INTO static_addresses (
client_pubkey,
@ -24,4 +29,9 @@ INSERT INTO static_addresses (
$6,
$7,
$8
);
);
-- name: GetLegacyAddress :one
SELECT * FROM static_addresses
ORDER BY id ASC
LIMIT 1;

View file

@ -13,22 +13,53 @@ import (
const allDeposits = `-- name: AllDeposits :many
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash, d.static_address_id,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
ORDER BY
id ASC
d.id ASC
`
func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) {
type AllDepositsRow struct {
ID int32
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
ClientPubkey []byte
ServerPubkey []byte
Expiry sql.NullInt32
ClientKeyFamily sql.NullInt32
ClientKeyIndex sql.NullInt32
Pkscript []byte
ProtocolVersion sql.NullInt32
InitiationHeight sql.NullInt32
}
func (q *Queries) AllDeposits(ctx context.Context) ([]AllDepositsRow, error) {
rows, err := q.db.QueryContext(ctx, allDeposits)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Deposit
var items []AllDepositsRow
for rows.Next() {
var i Deposit
var i AllDepositsRow
if err := rows.Scan(
&i.ID,
&i.DepositID,
@ -40,6 +71,15 @@ func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) {
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
&i.StaticAddressID,
&i.ClientPubkey,
&i.ServerPubkey,
&i.Expiry,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.Pkscript,
&i.ProtocolVersion,
&i.InitiationHeight,
); err != nil {
return nil, err
}
@ -63,7 +103,8 @@ INSERT INTO deposits (
confirmation_height,
timeout_sweep_pk_script,
expiry_sweep_txid,
finalized_withdrawal_tx
finalized_withdrawal_tx,
static_address_id
) VALUES (
$1,
$2,
@ -72,7 +113,8 @@ INSERT INTO deposits (
$5,
$6,
$7,
$8
$8,
$9
)
`
@ -85,6 +127,7 @@ type CreateDepositParams struct {
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
StaticAddressID sql.NullInt32
}
func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) error {
@ -97,15 +140,25 @@ func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) er
arg.TimeoutSweepPkScript,
arg.ExpirySweepTxid,
arg.FinalizedWithdrawalTx,
arg.StaticAddressID,
)
return err
}
const depositForOutpoint = `-- name: DepositForOutpoint :one
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash, d.static_address_id,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
WHERE
tx_hash = $1
AND
@ -117,9 +170,31 @@ type DepositForOutpointParams struct {
OutIndex int32
}
func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) {
type DepositForOutpointRow struct {
ID int32
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
ClientPubkey []byte
ServerPubkey []byte
Expiry sql.NullInt32
ClientKeyFamily sql.NullInt32
ClientKeyIndex sql.NullInt32
Pkscript []byte
ProtocolVersion sql.NullInt32
InitiationHeight sql.NullInt32
}
func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (DepositForOutpointRow, error) {
row := q.db.QueryRowContext(ctx, depositForOutpoint, arg.TxHash, arg.OutIndex)
var i Deposit
var i DepositForOutpointRow
err := row.Scan(
&i.ID,
&i.DepositID,
@ -131,22 +206,62 @@ func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpoint
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
&i.StaticAddressID,
&i.ClientPubkey,
&i.ServerPubkey,
&i.Expiry,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.Pkscript,
&i.ProtocolVersion,
&i.InitiationHeight,
)
return i, err
}
const getDeposit = `-- name: GetDeposit :one
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash, d.static_address_id,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height
FROM
deposits
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
WHERE
deposit_id = $1
`
func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) {
type GetDepositRow struct {
ID int32
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
ClientPubkey []byte
ServerPubkey []byte
Expiry sql.NullInt32
ClientKeyFamily sql.NullInt32
ClientKeyIndex sql.NullInt32
Pkscript []byte
ProtocolVersion sql.NullInt32
InitiationHeight sql.NullInt32
}
func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (GetDepositRow, error) {
row := q.db.QueryRowContext(ctx, getDeposit, depositID)
var i Deposit
var i GetDepositRow
err := row.Scan(
&i.ID,
&i.DepositID,
@ -158,6 +273,15 @@ func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, er
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
&i.StaticAddressID,
&i.ClientPubkey,
&i.ServerPubkey,
&i.Expiry,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.Pkscript,
&i.ProtocolVersion,
&i.InitiationHeight,
)
return i, err
}
@ -209,6 +333,17 @@ func (q *Queries) InsertDepositUpdate(ctx context.Context, arg InsertDepositUpda
return err
}
const setAllNullDepositsStaticAddressID = `-- name: SetAllNullDepositsStaticAddressID :exec
UPDATE deposits
SET static_address_id = $1
WHERE static_address_id IS NULL
`
func (q *Queries) SetAllNullDepositsStaticAddressID(ctx context.Context, staticAddressID sql.NullInt32) error {
_, err := q.db.ExecContext(ctx, setAllNullDepositsStaticAddressID, staticAddressID)
return err
}
const updateDeposit = `-- name: UpdateDeposit :exec
UPDATE deposits
SET

View file

@ -45,11 +45,20 @@ func (q *Queries) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([
const depositsForSwapHash = `-- name: DepositsForSwapHash :many
SELECT
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash,
d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash, d.static_address_id,
sa.client_pubkey client_pubkey,
sa.server_pubkey server_pubkey,
sa.expiry expiry,
sa.client_key_family client_key_family,
sa.client_key_index client_key_index,
sa.pkscript pkscript,
sa.protocol_version protocol_version,
sa.initiation_height initiation_height,
u.update_state,
u.update_timestamp
FROM
deposits d
LEFT JOIN static_addresses sa ON sa.id = d.static_address_id
LEFT JOIN
deposit_updates u ON u.id = (
SELECT id
@ -73,6 +82,15 @@ type DepositsForSwapHashRow struct {
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
ClientPubkey []byte
ServerPubkey []byte
Expiry sql.NullInt32
ClientKeyFamily sql.NullInt32
ClientKeyIndex sql.NullInt32
Pkscript []byte
ProtocolVersion sql.NullInt32
InitiationHeight sql.NullInt32
UpdateState sql.NullString
UpdateTimestamp sql.NullTime
}
@ -97,6 +115,15 @@ func (q *Queries) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]D
&i.ExpirySweepTxid,
&i.FinalizedWithdrawalTx,
&i.SwapHash,
&i.StaticAddressID,
&i.ClientPubkey,
&i.ServerPubkey,
&i.Expiry,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.Pkscript,
&i.ProtocolVersion,
&i.InitiationHeight,
&i.UpdateState,
&i.UpdateTimestamp,
); err != nil {
@ -153,14 +180,25 @@ func (q *Queries) GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([]
const getStaticAddressLoopInSwap = `-- name: GetStaticAddressLoopInSwap :one
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, static_address_swaps.change_static_address_id, static_address_swaps.confirmed_htlc_tx_id, static_address_swaps.confirmed_htlc_output_index, static_address_swaps.confirmed_htlc_output_value,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index,
change_address.client_pubkey change_client_pubkey,
change_address.server_pubkey change_server_pubkey,
change_address.expiry change_expiry,
change_address.client_key_family change_client_key_family,
change_address.client_key_index change_client_key_index,
change_address.pkscript change_pkscript,
change_address.protocol_version change_protocol_version,
change_address.initiation_height change_initiation_height
FROM
swaps
JOIN
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
LEFT JOIN
static_addresses change_address
ON static_address_swaps.change_static_address_id = change_address.id
WHERE
swaps.swap_hash = $1
`
@ -191,6 +229,10 @@ type GetStaticAddressLoopInSwapRow struct {
Fast bool
ConfirmationRiskDecision string
ConfirmationRiskDecisionTime sql.NullTime
ChangeStaticAddressID sql.NullInt32
ConfirmedHtlcTxID sql.NullString
ConfirmedHtlcOutputIndex sql.NullInt32
ConfirmedHtlcOutputValue sql.NullInt64
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -198,6 +240,14 @@ type GetStaticAddressLoopInSwapRow struct {
ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
ChangeClientPubkey []byte
ChangeServerPubkey []byte
ChangeExpiry sql.NullInt32
ChangeClientKeyFamily sql.NullInt32
ChangeClientKeyIndex sql.NullInt32
ChangePkscript []byte
ChangeProtocolVersion sql.NullInt32
ChangeInitiationHeight sql.NullInt32
}
func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) {
@ -229,6 +279,10 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
&i.Fast,
&i.ConfirmationRiskDecision,
&i.ConfirmationRiskDecisionTime,
&i.ChangeStaticAddressID,
&i.ConfirmedHtlcTxID,
&i.ConfirmedHtlcOutputIndex,
&i.ConfirmedHtlcOutputValue,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -236,6 +290,14 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
&i.ReceiverInternalPubkey,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.ChangeClientPubkey,
&i.ChangeServerPubkey,
&i.ChangeExpiry,
&i.ChangeClientKeyFamily,
&i.ChangeClientKeyIndex,
&i.ChangePkscript,
&i.ChangeProtocolVersion,
&i.ChangeInitiationHeight,
)
return i, err
}
@ -243,14 +305,25 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
const getStaticAddressLoopInSwapsByStates = `-- name: GetStaticAddressLoopInSwapsByStates :many
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, static_address_swaps.change_static_address_id, static_address_swaps.confirmed_htlc_tx_id, static_address_swaps.confirmed_htlc_output_index, static_address_swaps.confirmed_htlc_output_value,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index,
change_address.client_pubkey change_client_pubkey,
change_address.server_pubkey change_server_pubkey,
change_address.expiry change_expiry,
change_address.client_key_family change_client_key_family,
change_address.client_key_index change_client_key_index,
change_address.pkscript change_pkscript,
change_address.protocol_version change_protocol_version,
change_address.initiation_height change_initiation_height
FROM
swaps
JOIN
static_address_swaps ON swaps.swap_hash = static_address_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
LEFT JOIN
static_addresses change_address
ON static_address_swaps.change_static_address_id = change_address.id
JOIN
static_address_swap_updates u ON swaps.swap_hash = u.swap_hash
-- This subquery ensures that we are checking only the latest update for
@ -292,6 +365,10 @@ type GetStaticAddressLoopInSwapsByStatesRow struct {
Fast bool
ConfirmationRiskDecision string
ConfirmationRiskDecisionTime sql.NullTime
ChangeStaticAddressID sql.NullInt32
ConfirmedHtlcTxID sql.NullString
ConfirmedHtlcOutputIndex sql.NullInt32
ConfirmedHtlcOutputValue sql.NullInt64
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -299,6 +376,14 @@ type GetStaticAddressLoopInSwapsByStatesRow struct {
ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
ChangeClientPubkey []byte
ChangeServerPubkey []byte
ChangeExpiry sql.NullInt32
ChangeClientKeyFamily sql.NullInt32
ChangeClientKeyIndex sql.NullInt32
ChangePkscript []byte
ChangeProtocolVersion sql.NullInt32
ChangeInitiationHeight sql.NullInt32
}
func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) {
@ -336,6 +421,10 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla
&i.Fast,
&i.ConfirmationRiskDecision,
&i.ConfirmationRiskDecisionTime,
&i.ChangeStaticAddressID,
&i.ConfirmedHtlcTxID,
&i.ConfirmedHtlcOutputIndex,
&i.ConfirmedHtlcOutputValue,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -343,6 +432,14 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla
&i.ReceiverInternalPubkey,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.ChangeClientPubkey,
&i.ChangeServerPubkey,
&i.ChangeExpiry,
&i.ChangeClientKeyFamily,
&i.ChangeClientKeyIndex,
&i.ChangePkscript,
&i.ChangeProtocolVersion,
&i.ChangeInitiationHeight,
); err != nil {
return nil, err
}
@ -369,7 +466,8 @@ INSERT INTO static_address_swaps (
htlc_tx_fee_rate_sat_kw,
htlc_timeout_sweep_tx_id,
htlc_timeout_sweep_address,
fast
fast,
change_static_address_id
) VALUES (
$1,
$2,
@ -381,7 +479,8 @@ INSERT INTO static_address_swaps (
$8,
$9,
$10,
$11
$11,
$12
)
`
@ -397,6 +496,7 @@ type InsertStaticAddressLoopInParams struct {
HtlcTimeoutSweepTxID sql.NullString
HtlcTimeoutSweepAddress string
Fast bool
ChangeStaticAddressID sql.NullInt32
}
func (q *Queries) InsertStaticAddressLoopIn(ctx context.Context, arg InsertStaticAddressLoopInParams) error {
@ -412,6 +512,7 @@ func (q *Queries) InsertStaticAddressLoopIn(ctx context.Context, arg InsertStati
arg.HtlcTimeoutSweepTxID,
arg.HtlcTimeoutSweepAddress,
arg.Fast,
arg.ChangeStaticAddressID,
)
return err
}
@ -538,18 +639,31 @@ const updateStaticAddressLoopIn = `-- name: UpdateStaticAddressLoopIn :exec
UPDATE static_address_swaps
SET
htlc_tx_fee_rate_sat_kw = $2,
htlc_timeout_sweep_tx_id = $3
htlc_timeout_sweep_tx_id = $3,
confirmed_htlc_tx_id = $4,
confirmed_htlc_output_index = $5,
confirmed_htlc_output_value = $6
WHERE
swap_hash = $1
`
type UpdateStaticAddressLoopInParams struct {
SwapHash []byte
HtlcTxFeeRateSatKw int64
HtlcTimeoutSweepTxID sql.NullString
SwapHash []byte
HtlcTxFeeRateSatKw int64
HtlcTimeoutSweepTxID sql.NullString
ConfirmedHtlcTxID sql.NullString
ConfirmedHtlcOutputIndex sql.NullInt32
ConfirmedHtlcOutputValue sql.NullInt64
}
func (q *Queries) UpdateStaticAddressLoopIn(ctx context.Context, arg UpdateStaticAddressLoopInParams) error {
_, err := q.db.ExecContext(ctx, updateStaticAddressLoopIn, arg.SwapHash, arg.HtlcTxFeeRateSatKw, arg.HtlcTimeoutSweepTxID)
_, err := q.db.ExecContext(ctx, updateStaticAddressLoopIn,
arg.SwapHash,
arg.HtlcTxFeeRateSatKw,
arg.HtlcTimeoutSweepTxID,
arg.ConfirmedHtlcTxID,
arg.ConfirmedHtlcOutputIndex,
arg.ConfirmedHtlcOutputValue,
)
return err
}

View file

@ -11,6 +11,7 @@ import (
const allStaticAddresses = `-- name: AllStaticAddresses :many
SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses
ORDER BY id ASC
`
func (q *Queries) AllStaticAddresses(ctx context.Context) ([]StaticAddress, error) {
@ -93,6 +94,29 @@ func (q *Queries) CreateStaticAddress(ctx context.Context, arg CreateStaticAddre
return err
}
const getLegacyAddress = `-- name: GetLegacyAddress :one
SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses
ORDER BY id ASC
LIMIT 1
`
func (q *Queries) GetLegacyAddress(ctx context.Context) (StaticAddress, error) {
row := q.db.QueryRowContext(ctx, getLegacyAddress)
var i StaticAddress
err := row.Scan(
&i.ID,
&i.ClientPubkey,
&i.ServerPubkey,
&i.Expiry,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
&i.Pkscript,
&i.ProtocolVersion,
&i.InitiationHeight,
)
return i, err
}
const getStaticAddress = `-- name: GetStaticAddress :one
SELECT id, client_pubkey, server_pubkey, expiry, client_key_family, client_key_index, pkscript, protocol_version, initiation_height FROM static_addresses
WHERE pkscript=$1
@ -114,3 +138,15 @@ func (q *Queries) GetStaticAddress(ctx context.Context, pkscript []byte) (Static
)
return i, err
}
const getStaticAddressID = `-- name: GetStaticAddressID :one
SELECT id FROM static_addresses
WHERE pkscript=$1
`
func (q *Queries) GetStaticAddressID(ctx context.Context, pkscript []byte) (int32, error) {
row := q.db.QueryRowContext(ctx, getStaticAddressID, pkscript)
var id int32
err := row.Scan(&id)
return id, err
}

View file

@ -4907,9 +4907,14 @@ func (x *InstantOut) GetSweepTxId() string {
type NewStaticAddressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The client's public key for the 2-of-2 MuSig2 taproot static address.
ClientKey []byte `protobuf:"bytes,1,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
ClientKey []byte `protobuf:"bytes,1,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"`
// If set, loopd initiates a deposit by calling lnd's SendCoins API. If the
// request's addr field is empty, loopd creates and funds a new static
// address. If addr is set, it must be an existing static address known to
// loopd.
SendCoinsRequest *lnrpc.SendCoinsRequest `protobuf:"bytes,2,opt,name=send_coins_request,json=sendCoinsRequest,proto3" json:"send_coins_request,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NewStaticAddressRequest) Reset() {
@ -4949,14 +4954,23 @@ func (x *NewStaticAddressRequest) GetClientKey() []byte {
return nil
}
func (x *NewStaticAddressRequest) GetSendCoinsRequest() *lnrpc.SendCoinsRequest {
if x != nil {
return x.SendCoinsRequest
}
return nil
}
type NewStaticAddressResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The taproot static address.
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
// The CSV expiry of the static address.
Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"`
// The response from lnd's SendCoins API, if a deposit was initiated.
SendCoinsResponse *lnrpc.SendCoinsResponse `protobuf:"bytes,3,opt,name=send_coins_response,json=sendCoinsResponse,proto3" json:"send_coins_response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NewStaticAddressResponse) Reset() {
@ -5003,6 +5017,13 @@ func (x *NewStaticAddressResponse) GetExpiry() uint32 {
return 0
}
func (x *NewStaticAddressResponse) GetSendCoinsResponse() *lnrpc.SendCoinsResponse {
if x != nil {
return x.SendCoinsResponse
}
return nil
}
type ListUnspentDepositsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The number of minimum confirmations a utxo must have to be listed.
@ -5754,7 +5775,9 @@ type Deposit struct {
BlocksUntilExpiry int64 `protobuf:"varint,6,opt,name=blocks_until_expiry,json=blocksUntilExpiry,proto3" json:"blocks_until_expiry,omitempty"`
// The swap hash of the swap that this deposit is part of. This field is only
// set if the deposit is part of a loop-in swap.
SwapHash []byte `protobuf:"bytes,7,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
SwapHash []byte `protobuf:"bytes,7,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
// The static address that the deposit was sent to.
StaticAddress string `protobuf:"bytes,8,opt,name=static_address,json=staticAddress,proto3" json:"static_address,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -5838,6 +5861,13 @@ func (x *Deposit) GetSwapHash() []byte {
return nil
}
func (x *Deposit) GetStaticAddress() string {
if x != nil {
return x.StaticAddress
}
return ""
}
type StaticAddressWithdrawal struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The transaction id of the withdrawal transaction.
@ -7025,13 +7055,15 @@ const file_client_proto_rawDesc = "" +
"\x05state\x18\x02 \x01(\tR\x05state\x12\x16\n" +
"\x06amount\x18\x03 \x01(\x04R\x06amount\x12'\n" +
"\x0freservation_ids\x18\x04 \x03(\fR\x0ereservationIds\x12\x1e\n" +
"\vsweep_tx_id\x18\x05 \x01(\tR\tsweepTxId\"8\n" +
"\vsweep_tx_id\x18\x05 \x01(\tR\tsweepTxId\"\x7f\n" +
"\x17NewStaticAddressRequest\x12\x1d\n" +
"\n" +
"client_key\x18\x01 \x01(\fR\tclientKey\"L\n" +
"client_key\x18\x01 \x01(\fR\tclientKey\x12E\n" +
"\x12send_coins_request\x18\x02 \x01(\v2\x17.lnrpc.SendCoinsRequestR\x10sendCoinsRequest\"\x96\x01\n" +
"\x18NewStaticAddressResponse\x12\x18\n" +
"\aaddress\x18\x01 \x01(\tR\aaddress\x12\x16\n" +
"\x06expiry\x18\x02 \x01(\rR\x06expiry\"V\n" +
"\x06expiry\x18\x02 \x01(\rR\x06expiry\x12H\n" +
"\x13send_coins_response\x18\x03 \x01(\v2\x18.lnrpc.SendCoinsResponseR\x11sendCoinsResponse\"V\n" +
"\x1aListUnspentDepositsRequest\x12\x1b\n" +
"\tmin_confs\x18\x01 \x01(\x05R\bminConfs\x12\x1b\n" +
"\tmax_confs\x18\x02 \x01(\x05R\bmaxConfs\"B\n" +
@ -7075,7 +7107,7 @@ const file_client_proto_rawDesc = "" +
"\x18value_looped_in_satoshis\x18\b \x01(\x03R\x15valueLoopedInSatoshis\x12J\n" +
"\"value_htlc_timeout_sweeps_satoshis\x18\t \x01(\x03R\x1evalueHtlcTimeoutSweepsSatoshis\x122\n" +
"\x15value_channels_opened\x18\n" +
" \x01(\x03R\x13valueChannelsOpened\"\xf6\x01\n" +
" \x01(\x03R\x13valueChannelsOpened\"\x9d\x02\n" +
"\aDeposit\x12\x0e\n" +
"\x02id\x18\x01 \x01(\fR\x02id\x12+\n" +
"\x05state\x18\x02 \x01(\x0e2\x15.looprpc.DepositStateR\x05state\x12\x1a\n" +
@ -7083,7 +7115,8 @@ const file_client_proto_rawDesc = "" +
"\x05value\x18\x04 \x01(\x03R\x05value\x12/\n" +
"\x13confirmation_height\x18\x05 \x01(\x03R\x12confirmationHeight\x12.\n" +
"\x13blocks_until_expiry\x18\x06 \x01(\x03R\x11blocksUntilExpiry\x12\x1b\n" +
"\tswap_hash\x18\a \x01(\fR\bswapHash\"\xc2\x02\n" +
"\tswap_hash\x18\a \x01(\fR\bswapHash\x12%\n" +
"\x0estatic_address\x18\b \x01(\tR\rstaticAddress\"\xc2\x02\n" +
"\x17StaticAddressWithdrawal\x12\x13\n" +
"\x05tx_id\x18\x01 \x01(\tR\x04txId\x12,\n" +
"\bdeposits\x18\x02 \x03(\v2\x10.looprpc.DepositR\bdeposits\x12A\n" +
@ -7385,7 +7418,9 @@ var file_client_proto_goTypes = []any{
nil, // 89: looprpc.LiquidityParameters.EasyAssetParamsEntry
(*lnrpc.OpenChannelRequest)(nil), // 90: lnrpc.OpenChannelRequest
(*swapserverrpc.RouteHint)(nil), // 91: looprpc.RouteHint
(*lnrpc.OutPoint)(nil), // 92: lnrpc.OutPoint
(*lnrpc.SendCoinsRequest)(nil), // 92: lnrpc.SendCoinsRequest
(*lnrpc.SendCoinsResponse)(nil), // 93: lnrpc.SendCoinsResponse
(*lnrpc.OutPoint)(nil), // 94: lnrpc.OutPoint
}
var file_client_proto_depIdxs = []int32{
90, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest
@ -7425,92 +7460,94 @@ var file_client_proto_depIdxs = []int32{
51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified
57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation
64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut
69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo
92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint
7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState
80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit
81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal
82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap
7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState
80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit
8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState
80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit
91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint
80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit
87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint
87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint
46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams
14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest
27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest
28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest
36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest
42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest
12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest
44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest
58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest
60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest
62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest
65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest
67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest
70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest
72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest
74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest
76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest
78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest
83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest
10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest
16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse
18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse
30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse
37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse
43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse
13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse
45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse
59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse
61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse
63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse
66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse
68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse
71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse
73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse
75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse
77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse
79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse
84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse
11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse
85, // [85:118] is the sub-list for method output_type
52, // [52:85] is the sub-list for method input_type
52, // [52:52] is the sub-list for extension type_name
52, // [52:52] is the sub-list for extension extendee
0, // [0:52] is the sub-list for field type_name
92, // 37: looprpc.NewStaticAddressRequest.send_coins_request:type_name -> lnrpc.SendCoinsRequest
93, // 38: looprpc.NewStaticAddressResponse.send_coins_response:type_name -> lnrpc.SendCoinsResponse
69, // 39: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo
94, // 40: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint
7, // 41: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState
80, // 42: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit
81, // 43: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal
82, // 44: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap
7, // 45: looprpc.Deposit.state:type_name -> looprpc.DepositState
80, // 46: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit
8, // 47: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState
80, // 48: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit
91, // 49: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint
80, // 50: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit
87, // 51: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint
87, // 52: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint
46, // 53: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams
14, // 54: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest
15, // 55: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest
17, // 56: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest
19, // 57: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest
22, // 58: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest
27, // 59: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest
53, // 60: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest
28, // 61: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest
31, // 62: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest
28, // 63: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest
31, // 64: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest
34, // 65: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest
36, // 66: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest
36, // 67: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest
38, // 68: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest
42, // 69: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest
12, // 70: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest
44, // 71: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest
48, // 72: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest
50, // 73: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest
55, // 74: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest
58, // 75: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest
60, // 76: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest
62, // 77: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest
65, // 78: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest
67, // 79: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest
70, // 80: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest
72, // 81: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest
74, // 82: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest
76, // 83: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest
78, // 84: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest
83, // 85: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest
10, // 86: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest
16, // 87: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse
16, // 88: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse
18, // 89: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus
21, // 90: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse
23, // 91: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse
18, // 92: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus
54, // 93: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse
30, // 94: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse
33, // 95: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse
29, // 96: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse
32, // 97: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse
35, // 98: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse
37, // 99: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse
37, // 100: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse
39, // 101: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse
43, // 102: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse
13, // 103: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse
45, // 104: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters
49, // 105: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse
52, // 106: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse
56, // 107: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse
59, // 108: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse
61, // 109: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse
63, // 110: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse
66, // 111: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse
68, // 112: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse
71, // 113: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse
73, // 114: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse
75, // 115: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse
77, // 116: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse
79, // 117: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse
84, // 118: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse
11, // 119: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse
87, // [87:120] is the sub-list for method output_type
54, // [54:87] is the sub-list for method input_type
54, // [54:54] is the sub-list for extension type_name
54, // [54:54] is the sub-list for extension extendee
0, // [0:54] is the sub-list for field type_name
}
func init() { file_client_proto_init() }

View file

@ -163,7 +163,9 @@ service SwapClient {
returns (ListInstantOutsResponse);
/* loop: `static newstaticaddress`
NewStaticAddress requests a new static address for loop-ins from the server.
NewStaticAddress derives a fresh static receive address on every request
without send_coins_request.addr, or funds an existing address when addr is
set.
*/
rpc NewStaticAddress (NewStaticAddressRequest)
returns (NewStaticAddressResponse);
@ -1787,6 +1789,14 @@ message NewStaticAddressRequest {
The client's public key for the 2-of-2 MuSig2 taproot static address.
*/
bytes client_key = 1;
/*
If set, loopd initiates a deposit by calling lnd's SendCoins API. If the
request's addr field is empty, loopd creates and funds a new static
address. If addr is set, it must be an existing static address known to
loopd.
*/
lnrpc.SendCoinsRequest send_coins_request = 2;
}
message NewStaticAddressResponse {
@ -1799,6 +1809,11 @@ message NewStaticAddressResponse {
The CSV expiry of the static address.
*/
uint32 expiry = 2;
/*
The response from lnd's SendCoins API, if a deposit was initiated.
*/
lnrpc.SendCoinsResponse send_coins_response = 3;
}
message ListUnspentDepositsRequest {
@ -2097,6 +2112,11 @@ message Deposit {
set if the deposit is part of a loop-in swap.
*/
bytes swap_hash = 7;
/*
The static address that the deposit was sent to.
*/
string static_address = 8;
}
message StaticAddressWithdrawal {

View file

@ -870,7 +870,7 @@
},
"/v1/staticaddr": {
"post": {
"summary": "loop: `static newstaticaddress`\nNewStaticAddress requests a new static address for loop-ins from the server.",
"summary": "loop: `static newstaticaddress`\nNewStaticAddress derives a fresh static receive address on every request\nwithout send_coins_request.addr, or funds an existing address when addr is\nset.",
"operationId": "SwapClient_NewStaticAddress",
"responses": {
"200": {
@ -1204,6 +1204,16 @@
}
}
},
"lnrpcCoinSelectionStrategy": {
"type": "string",
"enum": [
"STRATEGY_USE_GLOBAL_CONFIG",
"STRATEGY_LARGEST",
"STRATEGY_RANDOM"
],
"default": "STRATEGY_USE_GLOBAL_CONFIG",
"description": " - STRATEGY_USE_GLOBAL_CONFIG: Use the coin selection strategy defined in the global configuration\n(lnd.conf).\n - STRATEGY_LARGEST: Select the largest available coins first during coin selection.\n - STRATEGY_RANDOM: Randomly select the available coins during coin selection."
},
"lnrpcCommitmentType": {
"type": "string",
"enum": [
@ -1436,6 +1446,73 @@
}
}
},
"lnrpcSendCoinsRequest": {
"type": "object",
"properties": {
"addr": {
"type": "string",
"title": "The address to send coins to"
},
"amount": {
"type": "string",
"format": "int64",
"title": "The amount in satoshis to send"
},
"target_conf": {
"type": "integer",
"format": "int32",
"description": "The target number of blocks that this transaction should be confirmed\nby."
},
"sat_per_vbyte": {
"type": "string",
"format": "uint64",
"description": "A manual fee rate set in sat/vbyte that should be used when crafting the\ntransaction."
},
"sat_per_byte": {
"type": "string",
"format": "int64",
"description": "Deprecated, use sat_per_vbyte.\nA manual fee rate set in sat/vbyte that should be used when crafting the\ntransaction."
},
"send_all": {
"type": "boolean",
"description": "If set, the amount field should be unset. It indicates lnd will send all\nwallet coins or all selected coins to the specified address."
},
"label": {
"type": "string",
"description": "An optional label for the transaction, limited to 500 characters."
},
"min_confs": {
"type": "integer",
"format": "int32",
"description": "The minimum number of confirmations each one of your outputs used for\nthe transaction must satisfy."
},
"spend_unconfirmed": {
"type": "boolean",
"description": "Whether unconfirmed outputs should be used as inputs for the transaction."
},
"coin_selection_strategy": {
"$ref": "#/definitions/lnrpcCoinSelectionStrategy",
"description": "The strategy to use for selecting coins."
},
"outpoints": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/lnrpcOutPoint"
},
"description": "A list of selected outpoints as inputs for the transaction."
}
}
},
"lnrpcSendCoinsResponse": {
"type": "object",
"properties": {
"txid": {
"type": "string",
"title": "The transaction ID of the transaction"
}
}
},
"looprpcAbandonSwapResponse": {
"type": "object"
},
@ -1620,6 +1697,10 @@
"type": "string",
"format": "byte",
"description": "The swap hash of the swap that this deposit is part of. This field is only\nset if the deposit is part of a loop-in swap."
},
"static_address": {
"type": "string",
"description": "The static address that the deposit was sent to."
}
}
},
@ -2520,6 +2601,10 @@
"type": "string",
"format": "byte",
"description": "The client's public key for the 2-of-2 MuSig2 taproot static address."
},
"send_coins_request": {
"$ref": "#/definitions/lnrpcSendCoinsRequest",
"description": "If set, loopd initiates a deposit by calling lnd's SendCoins API. If the\nrequest's addr field is empty, loopd creates and funds a new static\naddress. If addr is set, it must be an existing static address known to\nloopd."
}
}
},
@ -2534,6 +2619,10 @@
"type": "integer",
"format": "int64",
"description": "The CSV expiry of the static address."
},
"send_coins_response": {
"$ref": "#/definitions/lnrpcSendCoinsResponse",
"description": "The response from lnd's SendCoins API, if a deposit was initiated."
}
}
},

View file

@ -114,7 +114,9 @@ type SwapClientClient interface {
// their current status.
ListInstantOuts(ctx context.Context, in *ListInstantOutsRequest, opts ...grpc.CallOption) (*ListInstantOutsResponse, error)
// loop: `static newstaticaddress`
// NewStaticAddress requests a new static address for loop-ins from the server.
// NewStaticAddress derives a fresh static receive address on every request
// without send_coins_request.addr, or funds an existing address when addr is
// set.
NewStaticAddress(ctx context.Context, in *NewStaticAddressRequest, opts ...grpc.CallOption) (*NewStaticAddressResponse, error)
// loop: `static listunspentdeposits`
// ListUnspentDeposits returns a list of utxos deposited at a static address.
@ -574,7 +576,9 @@ type SwapClientServer interface {
// their current status.
ListInstantOuts(context.Context, *ListInstantOutsRequest) (*ListInstantOutsResponse, error)
// loop: `static newstaticaddress`
// NewStaticAddress requests a new static address for loop-ins from the server.
// NewStaticAddress derives a fresh static receive address on every request
// without send_coins_request.addr, or funds an existing address when addr is
// set.
NewStaticAddress(context.Context, *NewStaticAddressRequest) (*NewStaticAddressResponse, error)
// loop: `static listunspentdeposits`
// ListUnspentDeposits returns a list of utxos deposited at a static address.

View file

@ -82,7 +82,7 @@ var RequiredPermissions = map[string][]bakery.Op{
}},
"/looprpc.SwapClient/NewStaticAddress": {{
Entity: "swap",
Action: "read",
Action: "execute",
}, {
Entity: "loop",
Action: "in",

View file

@ -6,15 +6,26 @@ import (
"github.com/lightninglabs/loop/staticaddr/script"
)
// Parameters aliases the script-level static address parameters for callers
// that interact with the address manager API.
type Parameters = script.Parameters
// Store is the database interface that is used to store and retrieve
// static addresses.
type Store interface {
// CreateStaticAddress inserts a new static address with its parameters
// into the store.
CreateStaticAddress(ctx context.Context,
addrParams *script.Parameters) error
CreateStaticAddress(ctx context.Context, addrParams *Parameters) error
// GetStaticAddressID retrieves the static address row ID for the
// address script.
GetStaticAddressID(ctx context.Context, pkScript []byte) (int32, error)
// GetAllStaticAddresses retrieves all static addresses from the store.
GetAllStaticAddresses(ctx context.Context) ([]*script.Parameters,
error)
GetAllStaticAddresses(ctx context.Context) ([]*Parameters, error)
// GetLegacyParameters retrieves the first static address created for the
// L402. This is the immutable legacy/root address that anchors existing
// single-address deposits.
GetLegacyParameters(ctx context.Context) (*Parameters, error)
}

View file

@ -1,9 +1,11 @@
package address
import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
@ -29,6 +31,12 @@ const (
maxStaticAddressCSVExpiry = uint32(200 * 144)
)
var (
// ErrNoStaticAddress is returned when no static address parameters are
// present in the store.
ErrNoStaticAddress = errors.New("no static address parameters found")
)
// ManagerConfig holds the configuration for the address manager.
type ManagerConfig struct {
// AddressClient is the client that communicates with the loop server
@ -62,6 +70,12 @@ type Manager struct {
cfg *ManagerConfig
currentHeight atomic.Int32
// activeStaticAddresses is the runtime index used to match wallet UTXOs
// to locally known static address parameters. The DB remains the
// durable source of truth; this map is rebuilt from the DB on startup
// and updated after successful address issuance.
activeStaticAddresses map[string]*Parameters
}
// NewManager creates a new address manager.
@ -72,7 +86,8 @@ func NewManager(cfg *ManagerConfig, currentHeight int32) (*Manager, error) {
}
m := &Manager{
cfg: cfg,
cfg: cfg,
activeStaticAddresses: make(map[string]*Parameters),
}
m.currentHeight.Store(currentHeight)
@ -88,6 +103,11 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
return err
}
err = m.loadActiveAddresses(ctx)
if err != nil {
return err
}
// Communicate to the caller that the address manager has completed its
// initialization.
close(initChan)
@ -107,54 +127,125 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
}
}
// NewAddress creates a new static address with the server or returns an
// existing one.
// loadActiveAddresses rebuilds the runtime address map from the durable DB
// state and re-imports all scripts into lnd. Importing is intentionally
// idempotent so restart paths repair missing wallet watches before deposit
// discovery starts.
func (m *Manager) loadActiveAddresses(ctx context.Context) error {
params, err := m.cfg.Store.GetAllStaticAddresses(ctx)
if err != nil {
return err
}
active := make(map[string]*Parameters, len(params))
for _, param := range params {
staticAddress, err := staticAddressFromParams(param)
if err != nil {
return err
}
err = m.importAddressTapscript(ctx, staticAddress)
if err != nil {
return err
}
active[string(param.PkScript)] = param
}
m.Lock()
m.activeStaticAddresses = active
m.Unlock()
return nil
}
// NewAddress creates the next externally visible receive static address.
//
// The first call also makes sure the legacy/root static address seed exists,
// because receive and change addresses are derived from the server pubkey and
// expiry returned for that seed.
func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
int64, error) {
// If there's already a static address in the database, we can return
// it.
params, err := m.NewReceiveAddress(ctx)
if err != nil {
return nil, 0, err
}
address, err := m.GetTaprootAddress(
params.ClientPubkey, params.ServerPubkey, int64(params.Expiry),
)
if err != nil {
return nil, 0, err
}
return address, int64(params.Expiry), nil
}
// EnsureStaticAddressSeed loads or creates the legacy/root static address
// parameters. The root address is the only address that requires a Nautilus
// ServerNewAddress call; all receive/change addresses derive client keys
// locally and reuse this server pubkey/expiry seed.
func (m *Manager) EnsureStaticAddressSeed(ctx context.Context) (*Parameters,
error) {
m.Lock()
seed := m.legacyParameters()
m.Unlock()
if seed != nil {
return seed, nil
}
m.Lock()
defer m.Unlock()
// Another caller may have created the seed while we were waiting for the
// issuance lock.
seed = m.legacyParameters()
if seed != nil {
return seed, nil
}
addresses, err := m.cfg.Store.GetAllStaticAddresses(ctx)
if err != nil {
m.Unlock()
return nil, 0, err
return nil, err
}
if len(addresses) > 0 {
clientPubKey := addresses[0].ClientPubkey
serverPubKey := addresses[0].ServerPubkey
expiry := int64(addresses[0].Expiry)
for _, addr := range addresses {
// Re-import existing rows so startup can repair a DB-only
// address before deposit discovery depends on lnd's wallet
// view.
staticAddress, err := staticAddressFromParams(addr)
if err != nil {
return nil, err
}
defer m.Unlock()
err = m.importAddressTapscript(ctx, staticAddress)
if err != nil {
return nil, err
}
address, err := m.GetTaprootAddress(
clientPubKey, serverPubKey, expiry,
)
if err != nil {
return nil, 0, err
m.activeStaticAddresses[string(addr.PkScript)] = addr
}
return address, expiry, nil
return addresses[0], nil
}
m.Unlock()
// We are fetching a new L402 token from the server. There is one static
// address per L402 token allowed.
// We are fetching a new L402 token from the server. The returned server
// key/expiry is the static address seed for all future client-derived
// addresses for this L402.
err = m.cfg.FetchL402(ctx)
if err != nil {
return nil, 0, err
return nil, err
}
clientPubKey, err := m.cfg.WalletKit.DeriveNextKey(
ctx, swap.StaticAddressKeyFamily,
)
if err != nil {
return nil, 0, err
return nil, err
}
// Send our clientPubKey to the server and wait for the server to
// respond with he serverPubKey and the static address CSV expiry.
protocolVersion := version.CurrentRPCProtocolVersion()
resp, err := m.cfg.AddressClient.ServerNewAddress(
ctx, &staticaddressrpc.ServerNewAddressRequest{
@ -163,78 +254,121 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
},
)
if err != nil {
return nil, 0, err
return nil, err
}
if resp == nil {
return nil, 0, fmt.Errorf("missing server new address response")
return nil, fmt.Errorf("missing server new address response")
}
serverParams := resp.GetParams()
if err := validateServerAddressParams(serverParams); err != nil {
return nil, 0, err
return nil, err
}
serverPubKey, err := btcec.ParsePubKey(serverParams.GetServerKey())
if err != nil {
return nil, 0, err
return nil, err
}
return m.createAddressFromKey(
ctx, clientPubKey, serverPubKey, serverParams.Expiry,
version.AddressProtocolVersion(protocolVersion),
)
}
// NewReceiveAddress derives, stores, imports and activates the next receive
// family static address. It is used by `loop static new`.
func (m *Manager) NewReceiveAddress(ctx context.Context) (*Parameters, error) {
seed, err := m.EnsureStaticAddressSeed(ctx)
if err != nil {
return nil, err
}
return m.newDerivedAddress(ctx, seed, swap.StaticMultiAddressKeyFamily)
}
// NewChangeAddress derives, stores, imports and activates the next change
// family static address. Swap and withdrawal code calls this before submitting
// requests that require change.
func (m *Manager) NewChangeAddress(ctx context.Context) (*Parameters, error) {
seed, err := m.EnsureStaticAddressSeed(ctx)
if err != nil {
return nil, err
}
return m.newDerivedAddress(ctx, seed, swap.StaticAddressChangeKeyFamily)
}
func (m *Manager) newDerivedAddress(ctx context.Context, seed *Parameters,
keyFamily int32) (*Parameters, error) {
m.Lock()
defer m.Unlock()
clientPubKey, err := m.cfg.WalletKit.DeriveNextKey(ctx, keyFamily)
if err != nil {
return nil, err
}
return m.createAddressFromKey(
ctx, clientPubKey, seed.ServerPubkey, seed.Expiry,
seed.ProtocolVersion,
)
}
func (m *Manager) createAddressFromKey(ctx context.Context,
clientPubKey *keychain.KeyDescriptor, serverPubKey *btcec.PublicKey,
expiry uint32, protocolVersion version.AddressProtocolVersion) (
*Parameters, error) {
staticAddress, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(serverParams.Expiry),
clientPubKey.PubKey, serverPubKey,
input.MuSig2Version100RC2, int64(expiry), clientPubKey.PubKey,
serverPubKey,
)
if err != nil {
return nil, 0, err
return nil, err
}
pkScript, err := staticAddress.StaticAddressScript()
if err != nil {
return nil, 0, err
return nil, err
}
// Create the static address from the parameters the server provided and
// store all parameters in the database.
addrParams := &script.Parameters{
addrParams := &Parameters{
ClientPubkey: clientPubKey.PubKey,
ServerPubkey: serverPubKey,
PkScript: pkScript,
Expiry: serverParams.Expiry,
Expiry: expiry,
KeyLocator: keychain.KeyLocator{
Family: clientPubKey.Family,
Index: clientPubKey.Index,
},
ProtocolVersion: version.AddressProtocolVersion(
protocolVersion,
),
ProtocolVersion: protocolVersion,
InitiationHeight: m.currentHeight.Load(),
}
// Import before persisting the address row. If lnd rejects the script
// import, a later startup retry should still see a clean missing-address
// state instead of a DB-only static address.
err = m.importAddressTapscript(ctx, staticAddress)
if err != nil {
return nil, err
}
err = m.cfg.Store.CreateStaticAddress(ctx, addrParams)
if err != nil {
return nil, 0, err
return nil, err
}
// Import the static address tapscript into our lnd wallet, so we can
// track unspent outputs of it.
tapScript := input.TapscriptFullTree(
staticAddress.InternalPubKey, *staticAddress.TimeoutLeaf,
)
addr, err := m.cfg.WalletKit.ImportTaprootScript(ctx, tapScript)
addrParams.ID, err = m.cfg.Store.GetStaticAddressID(ctx, pkScript)
if err != nil {
return nil, 0, err
return nil, err
}
log.Infof("Imported static address taproot script to lnd wallet: %v",
addr)
m.activeStaticAddresses[string(pkScript)] = addrParams
address, err := m.GetTaprootAddress(
clientPubKey.PubKey, serverPubKey, int64(serverParams.Expiry),
)
if err != nil {
return nil, 0, err
}
return address, int64(serverParams.Expiry), nil
return addrParams, nil
}
// validateServerAddressParams validates the server-controlled static address
@ -272,6 +406,60 @@ func validateServerAddressParams(
return nil
}
func (m *Manager) importAddressTapscript(ctx context.Context,
staticAddress *script.StaticAddress) error {
// Import the static address tapscript into our lnd wallet, so we can
// track unspent outputs of it.
tapScript := input.TapscriptFullTree(
staticAddress.InternalPubKey, *staticAddress.TimeoutLeaf,
)
addr, err := m.cfg.WalletKit.ImportTaprootScript(ctx, tapScript)
if err != nil {
// Importing into an lnd instance that already knows the script is
// expected on restart. Treat the duplicate import as success.
if strings.Contains(err.Error(), "already exists") {
log.Infof("Static address tapscript already imported")
return nil
}
return err
}
log.Infof("Imported static address taproot script to lnd wallet: %v",
addr)
return nil
}
func staticAddressFromParams(params *Parameters) (*script.StaticAddress,
error) {
if params == nil {
return nil, fmt.Errorf("missing static address parameters")
}
return script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry),
params.ClientPubkey, params.ServerPubkey,
)
}
func (m *Manager) legacyParameters() *Parameters {
var legacy *Parameters
for _, params := range m.activeStaticAddresses {
if params == nil {
continue
}
if legacy == nil || params.ID < legacy.ID {
legacy = params
}
}
return legacy
}
// GetTaprootAddress returns a taproot address for the given client and server
// public keys and expiry.
func (m *Manager) GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,
@ -292,21 +480,17 @@ func (m *Manager) GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,
// ListUnspentRaw returns a list of utxos at the static address.
func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
maxConfs int32) (*btcutil.AddressTaproot, []*lnwallet.Utxo, error) {
maxConfs int32) ([]*lnwallet.Utxo, error) {
addresses, err := m.cfg.Store.GetAllStaticAddresses(ctx)
switch {
case err != nil:
return nil, nil, err
case len(addresses) == 0:
return nil, nil, nil
case len(addresses) > 1:
return nil, nil, fmt.Errorf("more than one address found")
m.Lock()
active := make(map[string]struct{}, len(m.activeStaticAddresses))
for pkScript := range m.activeStaticAddresses {
active[pkScript] = struct{}{}
}
m.Unlock()
if len(active) == 0 {
return nil, nil
}
staticAddress := addresses[0]
// List all unspent utxos the wallet sees, regardless of the number of
// confirmations.
@ -314,43 +498,36 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
ctx, minConfs, maxConfs,
)
if err != nil {
return nil, nil, err
return nil, err
}
// Filter the list of lnd's unspent utxos for the pkScript of our static
// address.
// Filter the list of lnd's unspent utxos for any locally active static
// address script.
var filteredUtxos []*lnwallet.Utxo
for _, utxo := range utxos {
if bytes.Equal(utxo.PkScript, staticAddress.PkScript) {
if _, ok := active[string(utxo.PkScript)]; ok {
filteredUtxos = append(filteredUtxos, utxo)
}
}
taprootAddress, err := m.GetTaprootAddress(
staticAddress.ClientPubkey, staticAddress.ServerPubkey,
int64(staticAddress.Expiry),
)
if err != nil {
return nil, nil, err
}
return taprootAddress, filteredUtxos, nil
return filteredUtxos, nil
}
// GetStaticAddressParameters returns the parameters of the static address.
// GetStaticAddressParameters returns the legacy/root static-address
// parameters.
func (m *Manager) GetStaticAddressParameters(ctx context.Context) (
*script.Parameters, error) {
params, err := m.cfg.Store.GetAllStaticAddresses(ctx)
params, err := m.GetLegacyParameters(ctx)
if err != nil {
return nil, err
}
if len(params) == 0 {
return nil, fmt.Errorf("no static address parameters found")
if params == nil {
return nil, ErrNoStaticAddress
}
return params[0], nil
return params, nil
}
// GetStaticAddress returns a taproot address for the given client and server
@ -363,25 +540,53 @@ func (m *Manager) GetStaticAddress(ctx context.Context) (*script.StaticAddress,
return nil, err
}
address, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry),
params.ClientPubkey, params.ServerPubkey,
)
if err != nil {
return nil, err
}
return address, nil
return staticAddressFromParams(params)
}
// ListUnspent returns a list of utxos at the static address.
func (m *Manager) ListUnspent(ctx context.Context, minConfs,
maxConfs int32) ([]*lnwallet.Utxo, error) {
_, utxos, err := m.ListUnspentRaw(ctx, minConfs, maxConfs)
return m.ListUnspentRaw(ctx, minConfs, maxConfs)
}
// GetLegacyParameters returns the legacy/root static address parameters.
func (m *Manager) GetLegacyParameters(ctx context.Context) (*Parameters,
error) {
params, err := m.cfg.Store.GetLegacyParameters(ctx)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return utxos, nil
return params, nil
}
// GetParameters returns active static address parameters for a pkScript.
func (m *Manager) GetParameters(pkScript []byte) *Parameters {
m.Lock()
defer m.Unlock()
return m.activeStaticAddresses[string(pkScript)]
}
// GetStaticAddressID returns the database row ID for a static address script.
func (m *Manager) GetStaticAddressID(ctx context.Context,
pkScript []byte) (int32, error) {
return m.cfg.Store.GetStaticAddressID(ctx, pkScript)
}
// IsOurPkScript returns true if the pkScript belongs to an active static
// address.
func (m *Manager) IsOurPkScript(pkScript []byte) bool {
return m.GetParameters(pkScript) != nil
}
// GetAllAddresses returns all persisted static address parameters.
func (m *Manager) GetAllAddresses(ctx context.Context) ([]*Parameters, error) {
return m.cfg.Store.GetAllStaticAddresses(ctx)
}

View file

@ -132,6 +132,20 @@ func TestManager(t *testing.T) {
// The expiry has to match.
require.EqualValues(t, defaultExpiry, expiry)
storedParams, err := testContext.manager.GetStaticAddressParameters(ctxb)
require.NoError(t, err)
require.EqualValues(
t, swap.StaticAddressKeyFamily, storedParams.KeyLocator.Family,
)
addresses, err := testContext.manager.GetAllAddresses(ctxb)
require.NoError(t, err)
require.Len(t, addresses, 2)
require.EqualValues(
t, swap.StaticMultiAddressKeyFamily,
addresses[1].KeyLocator.Family,
)
}
// TestNewAddressValidatesServerResponse tests that the untrusted
@ -233,12 +247,12 @@ func TestNewAddressAcceptsMaxCSVExpiry(t *testing.T) {
func GenerateExpectedTaprootAddress(t *ManagerTestContext) (
*btcutil.AddressTaproot, error) {
keyIndex := int32(0)
keyIndex := int32(1)
_, pubKey := test.CreateKey(keyIndex)
keyDescriptor := &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(swap.StaticAddressKeyFamily),
Family: keychain.KeyFamily(swap.StaticMultiAddressKeyFamily),
Index: uint32(keyIndex),
},
PubKey: pubKey,

View file

@ -6,7 +6,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/keychain"
)
@ -26,7 +25,7 @@ func NewSqlStore(db *loopdb.BaseDB) *SqlStore {
// CreateStaticAddress creates a static address record in the database.
func (s *SqlStore) CreateStaticAddress(ctx context.Context,
addrParams *script.Parameters) error {
addrParams *Parameters) error {
createArgs := sqlc.CreateStaticAddressParams{
ClientPubkey: addrParams.ClientPubkey.SerializeCompressed(),
@ -42,16 +41,23 @@ func (s *SqlStore) CreateStaticAddress(ctx context.Context,
return s.baseDB.Queries.CreateStaticAddress(ctx, createArgs)
}
// GetAllStaticAddresses returns all address known to the server.
// GetStaticAddressID retrieves the database ID for a static address script.
func (s *SqlStore) GetStaticAddressID(ctx context.Context,
pkScript []byte) (int32, error) {
return s.baseDB.Queries.GetStaticAddressID(ctx, pkScript)
}
// GetAllStaticAddresses returns all addresses known to the client.
func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) (
[]*script.Parameters, error) {
[]*Parameters, error) {
staticAddresses, err := s.baseDB.Queries.AllStaticAddresses(ctx)
if err != nil {
return nil, err
}
var result []*script.Parameters
var result []*Parameters
for _, address := range staticAddresses {
res, err := s.toAddressParameters(address)
if err != nil {
@ -64,10 +70,22 @@ func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) (
return result, nil
}
// GetLegacyParameters returns the first static address created for this L402.
func (s *SqlStore) GetLegacyParameters(ctx context.Context) (*Parameters,
error) {
staticAddress, err := s.baseDB.Queries.GetLegacyAddress(ctx)
if err != nil {
return nil, err
}
return s.toAddressParameters(staticAddress)
}
// toAddressParameters transforms a database representation of a static address
// to an AddressParameters struct.
func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) (
*script.Parameters, error) {
*Parameters, error) {
clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey)
if err != nil {
@ -79,7 +97,8 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) (
return nil, err
}
return &script.Parameters{
return &Parameters{
ID: row.ID,
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
PkScript: row.Pkscript,

View file

@ -6,7 +6,6 @@ import (
"fmt"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
@ -27,9 +26,15 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
msgTx := wire.NewMsgTx(2)
params, err := f.cfg.AddressManager.GetStaticAddressParameters(ctx)
if f.deposit.AddressParams == nil {
return f.HandleError(fmt.Errorf("missing static address " +
"parameters"))
}
params := f.deposit.AddressParams
address, err := f.deposit.GetStaticAddressScript()
if err != nil {
return fsm.OnError
return f.HandleError(err)
}
// Add the deposit outpoint as input to the transaction.
@ -96,11 +101,6 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
return f.HandleError(err)
}
address, err := f.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return f.HandleError(err)
}
sig := rawSigs[0]
msgTx.TxIn[0].Witness, err = address.GenTimeoutWitness(sig)
if err != nil {
@ -131,14 +131,10 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
func (f *FSM) WaitForExpirySweepAction(ctx context.Context,
_ fsm.EventContext) fsm.EventType {
var txID *chainhash.Hash
// Only pass the txid if we know it from our own publication.
if f.deposit.ExpirySweepTxid != (chainhash.Hash{}) {
txID = &f.deposit.ExpirySweepTxid
}
// Register by script only so an RBF replacement of the timeout sweep is
// still detected after restart with a stale ExpirySweepTxid.
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
ctx, txID, f.deposit.TimeOutSweepPkScript, DefaultConfTarget,
ctx, nil, f.deposit.TimeOutSweepPkScript, DefaultConfTarget,
int32(f.deposit.GetConfirmationHeight()),
)
if err != nil {

View file

@ -8,6 +8,8 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@ -52,6 +54,50 @@ func TestFinalizeDepositActionDoesNotBlock(t *testing.T) {
}
}
func TestWaitForExpirySweepActionRegistersByScriptOnly(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
timeoutPkScript := []byte{0x51, 0x20, 0x01}
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)
chainNotifier := &MockChainNotifier{}
chainNotifier.On(
"RegisterConfirmationsNtfn",
mock.Anything,
mock.MatchedBy(func(txid *chainhash.Hash) bool {
return txid == nil
}),
timeoutPkScript,
int32(DefaultConfTarget),
int32(42),
).Return(confChan, errChan, nil).Once()
depositFSM := &FSM{
cfg: &ManagerConfig{
ChainNotifier: chainNotifier,
},
deposit: &Deposit{
ConfirmationHeight: 42,
ExpirySweepTxid: chainhash.Hash{9},
TimeOutSweepPkScript: timeoutPkScript,
},
}
confirmedTx := wire.NewMsgTx(2)
confirmedTx.AddTxOut(&wire.TxOut{
Value: 1000,
PkScript: timeoutPkScript,
})
confChan <- &chainntnfs.TxConfirmation{Tx: confirmedTx}
event := depositFSM.WaitForExpirySweepAction(ctx, nil)
require.Equal(t, OnExpirySwept, event)
require.Equal(t, confirmedTx.TxHash(), depositFSM.deposit.ExpirySweepTxid)
chainNotifier.AssertExpectations(t)
}
// TestFinalizeDepositActionIgnoresRequestCancellation ensures the cleanup
// notification is tied to the FSM lifetime, not the caller's request context.
func TestFinalizeDepositActionIgnoresRequestCancellation(t *testing.T) {

View file

@ -9,6 +9,9 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
)
@ -70,6 +73,11 @@ type Deposit struct {
// FinalizedWithdrawalTx is the coop-signed withdrawal transaction. It
// is republished on new block arrivals and on client restarts.
FinalizedWithdrawalTx *wire.MsgTx
// AddressParams are the static address parameters that produced this
// deposit's pkScript. Spending code must use these per-deposit
// parameters rather than assuming all deposits belong to one address.
AddressParams *address.Parameters
}
// IsInFinalState returns true if the deposit is final.
@ -152,6 +160,19 @@ func (d *Deposit) GetConfirmationHeightNoLock() int64 {
return d.ConfirmationHeight
}
// GetStaticAddressScript reconstructs the static address script for this
// deposit's matched address parameters.
func (d *Deposit) GetStaticAddressScript() (*script.StaticAddress, error) {
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static address parameters")
}
return script.NewStaticAddress(
input.MuSig2Version100RC2, int64(d.AddressParams.Expiry),
d.AddressParams.ClientPubkey, d.AddressParams.ServerPubkey,
)
}
// GetRandomDepositID generates a random deposit ID.
func GetRandomDepositID() (ID, error) {
var id ID

View file

@ -181,13 +181,13 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
finalizedDepositChan chan wire.OutPoint,
recoverStateMachine bool) (*FSM, error) {
params, err := cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address "+
"parameters: %w", err)
if deposit.AddressParams == nil {
return nil, fmt.Errorf("missing deposit static address " +
"parameters")
}
params := deposit.AddressParams
address, err := cfg.AddressManager.GetStaticAddress(ctx)
address, err := deposit.GetStaticAddressScript()
if err != nil {
return nil, fmt.Errorf("unable to get static address: %w", err)
}
@ -535,10 +535,10 @@ func (f *FSM) Errorf(format string, args ...any) {
}
// SignDescriptor returns the sign descriptor for the static address output.
func (f *FSM) SignDescriptor(ctx context.Context) (*lndclient.SignDescriptor,
func (f *FSM) SignDescriptor(_ context.Context) (*lndclient.SignDescriptor,
error) {
address, err := f.cfg.AddressManager.GetStaticAddress(ctx)
address, err := f.deposit.GetStaticAddressScript()
if err != nil {
return nil, err
}
@ -546,10 +546,10 @@ func (f *FSM) SignDescriptor(ctx context.Context) (*lndclient.SignDescriptor,
return &lndclient.SignDescriptor{
WitnessScript: address.TimeoutLeaf.Script,
KeyDesc: keychain.KeyDescriptor{
PubKey: f.params.ClientPubkey,
PubKey: f.deposit.AddressParams.ClientPubkey,
},
Output: wire.NewTxOut(
int64(f.deposit.Value), f.params.PkScript,
int64(f.deposit.Value), f.deposit.AddressParams.PkScript,
),
HashType: txscript.SigHashDefault,
InputIndex: 0,

View file

@ -5,6 +5,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/lnwallet"
)
@ -39,6 +40,14 @@ type AddressManager interface {
GetStaticAddressParameters(ctx context.Context) (*script.Parameters,
error)
// GetStaticAddressID returns the database ID for the static address
// behind the given pkScript.
GetStaticAddressID(ctx context.Context, pkScript []byte) (int32, error)
// GetParameters returns active static address parameters for the given
// pkScript.
GetParameters(pkScript []byte) *address.Parameters
// GetStaticAddress returns the deposit address for the given
// client and server public keys.
GetStaticAddress(ctx context.Context) (*script.StaticAddress, error)

View file

@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
)
@ -69,8 +70,8 @@ type Manager struct {
// mu guards access to the activeDeposits map.
mu sync.Mutex
// reconcileMu serializes deposit reconciliation so new deposits are
// discovered and retained exactly once per outpoint.
// reconcileMu serializes startup recovery and deposit reconciliation so
// new deposits are discovered and retained exactly once per outpoint.
reconcileMu sync.Mutex
// activeDeposits contains all the active static address outputs.
@ -213,6 +214,9 @@ func (m *Manager) notifyActiveDeposits(ctx context.Context,
// recoverDeposits recovers static address parameters, previous deposits and
// state machines from the database and starts the deposit notifier.
func (m *Manager) recoverDeposits(ctx context.Context) error {
m.reconcileMu.Lock()
defer m.reconcileMu.Unlock()
log.Infof("Recovering static address parameters and deposits...")
// Recover deposits.
@ -222,6 +226,11 @@ func (m *Manager) recoverDeposits(ctx context.Context) error {
}
for i, d := range deposits {
err = m.hydrateLegacyDepositAddressParams(ctx, d)
if err != nil {
return err
}
m.deposits[d.OutPoint] = deposits[i]
// If the current deposit is final it wasn't active when we
@ -258,6 +267,66 @@ func (m *Manager) recoverDeposits(ctx context.Context) error {
return nil
}
// hydrateLegacyDepositAddressParams fills in address parameters for deposits
// that predate the durable deposit-to-static-address link. Those deposits all
// belonged to the legacy/root static address, so the legacy address manager
// lookup preserves the behavior that existed before multi-address support.
func (m *Manager) hydrateLegacyDepositAddressParams(ctx context.Context,
deposits ...*Deposit) error {
needsHydration := false
for _, d := range deposits {
if d != nil && d.AddressParams == nil {
needsHydration = true
break
}
}
if !needsHydration {
return nil
}
if m.cfg == nil || m.cfg.AddressManager == nil {
return nil
}
var legacyParams *address.Parameters
for _, d := range deposits {
if d == nil || d.AddressParams != nil {
continue
}
if legacyParams == nil {
params, err := m.cfg.AddressManager.
GetStaticAddressParameters(ctx)
if err != nil {
return fmt.Errorf("unable to load legacy "+
"static address parameters for deposit %v: %w",
d.OutPoint, err)
}
if params == nil {
return fmt.Errorf("missing legacy static address "+
"parameters for deposit %v", d.OutPoint)
}
if params.ID <= 0 {
params.ID, err = m.cfg.AddressManager.
GetStaticAddressID(ctx, params.PkScript)
if err != nil {
return fmt.Errorf("unable to load legacy "+
"static address ID for deposit %v: %w",
d.OutPoint, err)
}
}
legacyParams = params
}
d.AddressParams = legacyParams
}
return nil
}
// pollDeposits periodically polls for new deposits to our static address. This
// complements the block-driven reconciliation in the main event loop: while new
// blocks trigger reconcileDeposits to promptly detect confirmations, the ticker
@ -376,6 +445,17 @@ func (m *Manager) createNewDeposit(ctx context.Context,
if err != nil {
return nil, err
}
addressParams := m.cfg.AddressManager.GetParameters(utxo.PkScript)
if addressParams == nil {
return nil, fmt.Errorf("missing static address parameters "+
"for deposit %v", utxo.OutPoint)
}
if addressParams.ID <= 0 {
return nil, fmt.Errorf("missing static address ID for deposit %v",
utxo.OutPoint)
}
deposit := &Deposit{
ID: id,
state: Deposited,
@ -383,6 +463,7 @@ func (m *Manager) createNewDeposit(ctx context.Context,
Value: utxo.Value,
ConfirmationHeight: confirmationHeight,
TimeOutSweepPkScript: timeoutSweepPkScript,
AddressParams: addressParams,
}
err = m.cfg.Store.CreateDeposit(ctx, deposit)
@ -793,7 +874,17 @@ func (m *Manager) removeActiveDeposit(outpoint wire.OutPoint) {
// GetAllDeposits returns all known deposits from the database.
func (m *Manager) GetAllDeposits(ctx context.Context) ([]*Deposit, error) {
return m.cfg.Store.AllDeposits(ctx)
deposits, err := m.cfg.Store.AllDeposits(ctx)
if err != nil {
return nil, err
}
err = m.hydrateLegacyDepositAddressParams(ctx, deposits...)
if err != nil {
return nil, err
}
return deposits, nil
}
// GetVisibleDeposits returns deposits that should be exposed through normal
@ -808,6 +899,11 @@ func (m *Manager) GetVisibleDeposits(ctx context.Context) ([]*Deposit, error) {
return nil, err
}
err = m.hydrateLegacyDepositAddressParams(ctx, deposits...)
if err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
@ -884,6 +980,11 @@ func (m *Manager) DepositsForOutpoints(ctx context.Context,
return nil, err
}
err = m.hydrateLegacyDepositAddressParams(ctx, deposit)
if err != nil {
return nil, err
}
deposits = append(deposits, deposit)
}

View file

@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/test"
@ -41,8 +42,15 @@ func TestReconcileDepositsSerialized(t *testing.T) {
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
).Return([]*lnwallet.Utxo{utxo}, nil)
mockAddressManager.On(
"GetStaticAddressParameters", mock.Anything,
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
"GetParameters", mock.Anything,
).Return(&address.Parameters{
ID: 1,
ClientPubkey: defaultServerPubkey,
ServerPubkey: defaultServerPubkey,
Expiry: defaultExpiry,
PkScript: utxo.PkScript,
ProtocolVersion: 999,
})
mockStore := new(mockStore)
var createCalls atomic.Int32
@ -137,8 +145,15 @@ func TestReconcileConfirmedDepositUsesCurrentHeight(t *testing.T) {
"ListUnspent", mock.Anything, int32(0), int32(MaxConfs),
).Return([]*lnwallet.Utxo{utxo}, nil)
mockAddressManager.On(
"GetStaticAddressParameters", mock.Anything,
).Return((*script.Parameters)(nil), errors.New("fsm init failed"))
"GetParameters", mock.Anything,
).Return(&address.Parameters{
ID: 1,
ClientPubkey: defaultServerPubkey,
ServerPubkey: defaultServerPubkey,
Expiry: defaultExpiry,
PkScript: utxo.PkScript,
ProtocolVersion: 999,
})
mockStore := new(mockStore)
mockStore.On(
@ -471,6 +486,12 @@ func TestReconcileDepositsReactivatesReappearedDeposit(t *testing.T) {
OutPoint: outpoint,
Value: btcutil.Amount(100_000),
ConfirmationHeight: 77,
AddressParams: &address.Parameters{
ClientPubkey: defaultServerPubkey,
ServerPubkey: defaultServerPubkey,
Expiry: defaultExpiry,
ProtocolVersion: version.ProtocolVersion_V0,
},
}
deposit.SetState(Deposited)

View file

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
@ -112,6 +113,16 @@ type mockAddressManager struct {
mock.Mock
}
func (m *mockAddressManager) hasExpectation(method string) bool {
for _, call := range m.ExpectedCalls {
if call.Method == method {
return true
}
}
return false
}
func (m *mockAddressManager) GetStaticAddressParameters(ctx context.Context) (
*script.Parameters, error) {
@ -121,6 +132,39 @@ func (m *mockAddressManager) GetStaticAddressParameters(ctx context.Context) (
args.Error(1)
}
func (m *mockAddressManager) GetStaticAddressID(ctx context.Context,
pkScript []byte) (int32, error) {
if !m.hasExpectation("GetStaticAddressID") {
return 1, nil
}
args := m.Called(ctx, pkScript)
return int32(args.Int(0)), args.Error(1)
}
func (m *mockAddressManager) GetParameters(
pkScript []byte) *address.Parameters {
if !m.hasExpectation("GetParameters") {
return &address.Parameters{
ID: 1,
ClientPubkey: defaultServerPubkey,
ServerPubkey: defaultServerPubkey,
Expiry: defaultExpiry,
PkScript: pkScript,
}
}
args := m.Called(pkScript)
if args.Get(0) == nil {
return nil
}
return args.Get(0).(*address.Parameters)
}
func (m *mockAddressManager) GetStaticAddress(ctx context.Context) (
*script.StaticAddress, error) {
@ -481,6 +525,96 @@ func TestManagerSkipsExpiryNotificationOnReconcileFailure(t *testing.T) {
}
}
func TestRecoverDepositsKeepsSpentWithdrawing(t *testing.T) {
ctx := context.Background()
id, err := GetRandomDepositID()
require.NoError(t, err)
storedDeposit := &Deposit{
ID: id,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{2},
Index: 2,
},
state: Withdrawing,
Value: btcutil.Amount(100000),
ConfirmationHeight: 42,
}
testContext := newManagerTestContextWithStoredDeposits(
t, []*Deposit{storedDeposit}, nil,
)
err = testContext.manager.recoverDeposits(ctx)
require.NoError(t, err)
deposits, err := testContext.manager.GetActiveDepositsInState(Withdrawing)
require.NoError(t, err)
require.Len(t, deposits, 1)
require.Equal(t, storedDeposit.OutPoint, deposits[0].OutPoint)
}
func TestRecoverDepositsHydratesLegacyAddressParams(t *testing.T) {
ctx := context.Background()
id, err := GetRandomDepositID()
require.NoError(t, err)
storedDeposit := &Deposit{
ID: id,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{3},
Index: 3,
},
state: Deposited,
Value: btcutil.Amount(100000),
ConfirmationHeight: 42,
TimeOutSweepPkScript: []byte{0x42, 0x21, 0x69},
}
testContext := newManagerTestContextWithStoredDeposits(
t, []*Deposit{storedDeposit}, nil,
)
storedDeposit.AddressParams = nil
err = testContext.manager.recoverDeposits(ctx)
require.NoError(t, err)
require.NotNil(t, storedDeposit.AddressParams)
require.NotZero(t, storedDeposit.AddressParams.ID)
}
func TestGetAllDepositsHydratesLegacyAddressParams(t *testing.T) {
ctx := context.Background()
id, err := GetRandomDepositID()
require.NoError(t, err)
storedDeposit := &Deposit{
ID: id,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{4},
Index: 4,
},
state: Withdrawn,
Value: btcutil.Amount(100000),
ConfirmationHeight: 42,
}
testContext := newManagerTestContextWithStoredDeposits(
t, []*Deposit{storedDeposit}, nil,
)
storedDeposit.AddressParams = nil
deposits, err := testContext.manager.GetAllDeposits(ctx)
require.NoError(t, err)
require.Len(t, deposits, 1)
require.NotNil(t, deposits[0].AddressParams)
require.NotZero(t, deposits[0].AddressParams.ID)
}
// ManagerTestContext is a helper struct that contains all the necessary
// components to test the reservation manager.
type ManagerTestContext struct {
@ -497,6 +631,39 @@ type ManagerTestContext struct {
// newManagerTestContext creates a new test context for the reservation manager.
func newManagerTestContext(t *testing.T) *ManagerTestContext {
ID, err := GetRandomDepositID()
require.NoError(t, err)
utxo := &lnwallet.Utxo{
AddressType: lnwallet.TaprootPubkey,
Value: btcutil.Amount(100000),
Confirmations: int64(defaultDepositConfirmations),
PkScript: []byte("pkscript"),
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0xffffffff,
},
}
storedDeposits := []*Deposit{
{
ID: ID,
state: Deposited,
OutPoint: utxo.OutPoint,
Value: utxo.Value,
ConfirmationHeight: 3,
TimeOutSweepPkScript: []byte{0x42, 0x21, 0x69},
},
}
return newManagerTestContextWithStoredDeposits(
t, storedDeposits, []*lnwallet.Utxo{utxo},
)
}
func newManagerTestContextWithStoredDeposits(t *testing.T,
storedDeposits []*Deposit, utxos []*lnwallet.Utxo) *ManagerTestContext {
mockLnd := test.NewMockLnd()
lndContext := test.NewContext(t, mockLnd)
@ -509,29 +676,6 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext {
blockChan := make(chan int32)
blockErrChan := make(chan error)
ID, err := GetRandomDepositID()
utxo := &lnwallet.Utxo{
AddressType: lnwallet.TaprootPubkey,
Value: btcutil.Amount(100000),
Confirmations: int64(defaultDepositConfirmations),
PkScript: []byte("pkscript"),
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0xffffffff,
},
}
require.NoError(t, err)
storedDeposits := []*Deposit{
{
ID: ID,
state: Deposited,
OutPoint: utxo.OutPoint,
Value: utxo.Value,
ConfirmationHeight: 3,
TimeOutSweepPkScript: []byte{0x42, 0x21, 0x69},
},
}
mockStore.On(
"AllDeposits", mock.Anything,
).Return(storedDeposits, nil)
@ -540,17 +684,29 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext {
"UpdateDeposit", mock.Anything, mock.Anything,
).Return(nil)
staticAddress, addrParams := generateStaticAddress(
context.Background(), mockLnd, lndContext.T,
)
for _, storedDeposit := range storedDeposits {
if storedDeposit.AddressParams == nil {
storedDeposit.AddressParams = addrParams
}
}
var manager *Manager
mockAddressManager.On(
"GetStaticAddressParameters", mock.Anything,
).Return(&script.Parameters{
Expiry: defaultExpiry,
}, nil)
).Return(addrParams, nil)
mockAddressManager.On(
"ListUnspent", mock.Anything, mock.Anything, mock.Anything,
).Return(func() []*lnwallet.Utxo {
currentUtxo := *utxo
if len(utxos) != 1 {
return utxos
}
currentUtxo := *utxos[0]
currentHeight := manager.currentHeight.Load()
if currentHeight < defaultDepositConfirmations {
currentUtxo.Confirmations = 0
@ -595,9 +751,6 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext {
blockErrChan: blockErrChan,
}
staticAddress := generateStaticAddress(
context.Background(), testContext,
)
mockAddressManager.On(
"GetStaticAddress", mock.Anything,
).Return(staticAddress, nil)
@ -605,19 +758,30 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext {
return testContext
}
func generateStaticAddress(ctx context.Context,
t *ManagerTestContext) *script.StaticAddress {
func generateStaticAddress(ctx context.Context, mockLnd *test.LndMockServices,
t *testing.T) (*script.StaticAddress, *address.Parameters) {
keyDescriptor, err := t.mockLnd.WalletKit.DeriveNextKey(
keyDescriptor, err := mockLnd.WalletKit.DeriveNextKey(
ctx, swap.StaticAddressKeyFamily,
)
require.NoError(t.context.T, err)
require.NoError(t, err)
staticAddress, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(defaultExpiry),
keyDescriptor.PubKey, defaultServerPubkey,
)
require.NoError(t.context.T, err)
require.NoError(t, err)
return staticAddress
pkScript, err := staticAddress.StaticAddressScript()
require.NoError(t, err)
return staticAddress, &address.Parameters{
ID: 1,
ClientPubkey: keyDescriptor.PubKey,
ServerPubkey: defaultServerPubkey,
Expiry: defaultExpiry,
PkScript: pkScript,
KeyLocator: keyDescriptor.KeyLocator,
ProtocolVersion: 0,
}
}

View file

@ -6,14 +6,19 @@ import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
)
@ -49,6 +54,17 @@ func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error {
Amount: int64(deposit.Value),
ConfirmationHeight: deposit.GetConfirmationHeight(),
TimeoutSweepPkScript: deposit.TimeOutSweepPkScript,
StaticAddressID: sql.NullInt32{},
}
if deposit.AddressParams != nil {
if deposit.AddressParams.ID <= 0 {
return fmt.Errorf("static address ID must be set")
}
createArgs.StaticAddressID = sql.NullInt32{
Int32: deposit.AddressParams.ID,
Valid: true,
}
}
updateArgs := sqlc.InsertDepositUpdateParams{
@ -147,7 +163,9 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) {
return err
}
deposit, err = ToDeposit(row, latestUpdate)
deposit, err = toDeposit(
depositRowFromGet(row), latestUpdate,
)
if err != nil {
return err
}
@ -193,7 +211,9 @@ func (s *SqlStore) DepositForOutpoint(ctx context.Context,
return err
}
deposit, err = ToDeposit(row, latestUpdate)
deposit, err = toDeposit(
depositRowFromOutpoint(row), latestUpdate,
)
if err != nil {
return err
}
@ -245,8 +265,105 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) {
return allDeposits, nil
}
// ToDeposit converts an sql deposit to a deposit.
func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit,
// ToDeposit converts an sql deposit row with joined static address metadata to
// a deposit.
func ToDeposit(row sqlc.AllDepositsRow, lastUpdate sqlc.DepositUpdate) (*Deposit,
error) {
return toDeposit(depositRowFromAll(row), lastUpdate)
}
type depositRow struct {
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
FinalizedWithdrawalTx sql.NullString
SwapHash []byte
StaticAddressID sql.NullInt32
ClientPubkey []byte
ServerPubkey []byte
Expiry sql.NullInt32
ClientKeyFamily sql.NullInt32
ClientKeyIndex sql.NullInt32
Pkscript []byte
ProtocolVersion sql.NullInt32
InitiationHeight sql.NullInt32
}
func depositRowFromAll(row sqlc.AllDepositsRow) depositRow {
return depositRow{
DepositID: row.DepositID,
TxHash: row.TxHash,
OutIndex: row.OutIndex,
Amount: row.Amount,
ConfirmationHeight: row.ConfirmationHeight,
TimeoutSweepPkScript: row.TimeoutSweepPkScript,
ExpirySweepTxid: row.ExpirySweepTxid,
FinalizedWithdrawalTx: row.FinalizedWithdrawalTx,
SwapHash: row.SwapHash,
StaticAddressID: row.StaticAddressID,
ClientPubkey: row.ClientPubkey,
ServerPubkey: row.ServerPubkey,
Expiry: row.Expiry,
ClientKeyFamily: row.ClientKeyFamily,
ClientKeyIndex: row.ClientKeyIndex,
Pkscript: row.Pkscript,
ProtocolVersion: row.ProtocolVersion,
InitiationHeight: row.InitiationHeight,
}
}
func depositRowFromGet(row sqlc.GetDepositRow) depositRow {
return depositRow{
DepositID: row.DepositID,
TxHash: row.TxHash,
OutIndex: row.OutIndex,
Amount: row.Amount,
ConfirmationHeight: row.ConfirmationHeight,
TimeoutSweepPkScript: row.TimeoutSweepPkScript,
ExpirySweepTxid: row.ExpirySweepTxid,
FinalizedWithdrawalTx: row.FinalizedWithdrawalTx,
SwapHash: row.SwapHash,
StaticAddressID: row.StaticAddressID,
ClientPubkey: row.ClientPubkey,
ServerPubkey: row.ServerPubkey,
Expiry: row.Expiry,
ClientKeyFamily: row.ClientKeyFamily,
ClientKeyIndex: row.ClientKeyIndex,
Pkscript: row.Pkscript,
ProtocolVersion: row.ProtocolVersion,
InitiationHeight: row.InitiationHeight,
}
}
func depositRowFromOutpoint(row sqlc.DepositForOutpointRow) depositRow {
return depositRow{
DepositID: row.DepositID,
TxHash: row.TxHash,
OutIndex: row.OutIndex,
Amount: row.Amount,
ConfirmationHeight: row.ConfirmationHeight,
TimeoutSweepPkScript: row.TimeoutSweepPkScript,
ExpirySweepTxid: row.ExpirySweepTxid,
FinalizedWithdrawalTx: row.FinalizedWithdrawalTx,
SwapHash: row.SwapHash,
StaticAddressID: row.StaticAddressID,
ClientPubkey: row.ClientPubkey,
ServerPubkey: row.ServerPubkey,
Expiry: row.Expiry,
ClientKeyFamily: row.ClientKeyFamily,
ClientKeyIndex: row.ClientKeyIndex,
Pkscript: row.Pkscript,
ProtocolVersion: row.ProtocolVersion,
InitiationHeight: row.InitiationHeight,
}
}
func toDeposit(row depositRow, lastUpdate sqlc.DepositUpdate) (*Deposit,
error) {
id := ID{}
@ -296,7 +413,7 @@ func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit,
swapHash = &hash
}
return &Deposit{
deposit := &Deposit{
ID: id,
state: fsm.StateType(lastUpdate.UpdateState),
OutPoint: wire.OutPoint{
@ -309,5 +426,57 @@ func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit,
ExpirySweepTxid: expirySweepTxid,
SwapHash: swapHash,
FinalizedWithdrawalTx: finalizedWithdrawalTx,
}, nil
}
if row.StaticAddressID.Valid {
clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey)
if err != nil {
return nil, err
}
serverPubkey, err := btcec.ParsePubKey(row.ServerPubkey)
if err != nil {
return nil, err
}
deposit.AddressParams = &address.Parameters{
ID: row.StaticAddressID.Int32,
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
Expiry: uint32(row.Expiry.Int32),
PkScript: row.Pkscript,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(
row.ClientKeyFamily.Int32,
),
Index: uint32(row.ClientKeyIndex.Int32),
},
ProtocolVersion: version.AddressProtocolVersion(
row.ProtocolVersion.Int32,
),
InitiationHeight: row.InitiationHeight.Int32,
}
}
return deposit, nil
}
// BatchSetStaticAddressID sets the static address id for all deposits that
// predate the deposit-to-address schema link.
func (s *SqlStore) BatchSetStaticAddressID(ctx context.Context,
staticAddressID int32) error {
if staticAddressID <= 0 {
return fmt.Errorf("static address ID must be set")
}
return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
func(q *sqlc.Queries) error {
return q.SetAllNullDepositsStaticAddressID(
ctx, sql.NullInt32{
Int32: staticAddressID,
Valid: true,
},
)
})
}

View file

@ -1,6 +1,7 @@
package deposit
import (
"context"
"database/sql"
"testing"
@ -8,10 +9,21 @@ import (
"github.com/jackc/pgx/v5"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
func TestCreateDepositRejectsUnpersistedAddress(t *testing.T) {
store := NewSqlStore(nil)
deposit := &Deposit{
AddressParams: &script.Parameters{},
}
err := store.CreateDeposit(context.Background(), deposit)
require.ErrorContains(t, err, "static address ID must be set")
}
func TestToDeposit(t *testing.T) {
depositID, err := GetRandomDepositID()
require.NoError(t, err)
@ -24,13 +36,13 @@ func TestToDeposit(t *testing.T) {
tests := []struct {
name string
row sqlc.Deposit
row sqlc.AllDepositsRow
lastUpdate sqlc.DepositUpdate
expectErr bool
}{
{
name: "fully valid data",
row: sqlc.Deposit{
row: sqlc.AllDepositsRow{
DepositID: depositID[:],
TxHash: txHash[:],
Amount: 100000000,
@ -44,7 +56,7 @@ func TestToDeposit(t *testing.T) {
},
{
name: "fully valid data",
row: sqlc.Deposit{
row: sqlc.AllDepositsRow{
DepositID: depositID[:],
TxHash: txHash[:],
Amount: 100000000,

View file

@ -1,6 +1,7 @@
package loopin
import (
"bytes"
"context"
"crypto/rand"
"errors"
@ -109,6 +110,29 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
}
swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee
var changeOutput *swapserverrpc.StaticAddressChangeOutput
if hasChange {
changeAmount := f.loopIn.ExpectedChangeAmount()
f.loopIn.ChangeAddressParams, err =
f.cfg.AddressManager.NewChangeAddress(ctx)
if err != nil {
err = fmt.Errorf("unable to create static address "+
"change output: %w", err)
return returnError(err)
}
changeOutput, err = staticutil.ChangeOutput(
f.loopIn.ChangeAddressParams, changeAmount,
)
if err != nil {
err = fmt.Errorf("unable to prepare static address "+
"change output: %w", err)
return returnError(err)
}
}
// Generate random preimage.
var swapPreimage lntypes.Preimage
if _, err = rand.Read(swapPreimage[:]); err != nil {
@ -158,16 +182,28 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
version.CurrentRPCProtocolVersion(),
)
depositClientPubkeys, err := staticutil.DepositClientPubkeys(
f.loopIn.Deposits,
)
if err != nil {
err = fmt.Errorf("unable to prepare static address input "+
"proofs: %w", err)
return returnError(err)
}
loopInReq := &swapserverrpc.ServerStaticAddressLoopInRequest{
SwapHash: f.loopIn.SwapHash[:],
DepositOutpoints: f.loopIn.DepositOutpoints,
Amount: uint64(f.loopIn.SelectedAmount),
HtlcClientPubKey: f.loopIn.ClientPubkey.SerializeCompressed(),
SwapInvoice: f.loopIn.SwapInvoice,
ProtocolVersion: version.CurrentRPCProtocolVersion(),
UserAgent: loop.UserAgent(f.loopIn.Initiator),
PaymentTimeoutSeconds: f.loopIn.PaymentTimeoutSeconds,
Fast: f.loopIn.Fast,
SwapHash: f.loopIn.SwapHash[:],
DepositOutpoints: f.loopIn.DepositOutpoints,
Amount: uint64(f.loopIn.SelectedAmount),
HtlcClientPubKey: f.loopIn.ClientPubkey.SerializeCompressed(),
SwapInvoice: f.loopIn.SwapInvoice,
ProtocolVersion: version.CurrentRPCProtocolVersion(),
UserAgent: loop.UserAgent(f.loopIn.Initiator),
PaymentTimeoutSeconds: f.loopIn.PaymentTimeoutSeconds,
Fast: f.loopIn.Fast,
DepositToClientPubkeys: depositClientPubkeys,
ChangeOutput: changeOutput,
}
if f.loopIn.LastHop != nil {
loopInReq.LastHop = f.loopIn.LastHop
@ -601,8 +637,7 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
// rates.
createSession := staticutil.CreateMusig2Sessions
htlcSessions, clientHtlcNonces, err := createSession(
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
ctx, f.cfg.Signer, f.loopIn.Deposits,
)
if err != nil {
err = fmt.Errorf("unable to create musig2 sessions: %w", err)
@ -612,8 +647,7 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessions)
htlcSessionsHighFee, highFeeNonces, err := createSession(
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
ctx, f.cfg.Signer, f.loopIn.Deposits,
)
if err != nil {
return f.HandleError(err)
@ -621,8 +655,7 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessionsHighFee)
htlcSessionsExtremelyHighFee, extremelyHighNonces, err := createSession(
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
ctx, f.cfg.Signer, f.loopIn.Deposits,
)
if err != nil {
err = fmt.Errorf("unable to convert nonces: %w", err)
@ -1139,9 +1172,14 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
htlcConfirmed := false
for {
select {
case <-htlcConfChan:
case conf := <-htlcConfChan:
f.Infof("htlc tx confirmed")
err = f.recordConfirmedHtlc(ctx, conf, htlc.PkScript)
if err != nil {
return f.HandleError(err)
}
htlcConfirmed = true
if invoiceCanceledForNonPayment {
err = transitionDepositsToHtlcTimeout(
@ -1182,6 +1220,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
// confirmation and re-register for the next
// confirmation.
htlcConfirmed = false
err = f.clearConfirmedHtlc(ctx)
if err != nil {
return f.HandleError(err)
}
htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
if err != nil {
@ -1359,6 +1401,51 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
}
}
func (f *FSM) recordConfirmedHtlc(ctx context.Context,
conf *chainntnfs.TxConfirmation, htlcPkScript []byte) error {
if conf == nil || conf.Tx == nil {
return errors.New("htlc confirmation missing transaction")
}
if f.cfg.Store == nil {
return errors.New("missing static address loop-in store")
}
tx := conf.Tx
txHash := tx.TxHash()
for idx, txOut := range tx.TxOut {
if !bytes.Equal(txOut.PkScript, htlcPkScript) {
continue
}
f.loopIn.HtlcTxHash = &txHash
f.loopIn.HtlcOutputIndex = uint32(idx)
f.loopIn.HtlcOutputValue = btcutil.Amount(txOut.Value)
return f.cfg.Store.UpdateLoopIn(ctx, f.loopIn)
}
return fmt.Errorf("confirmed htlc tx %v missing expected htlc "+
"output", txHash)
}
func (f *FSM) clearConfirmedHtlc(ctx context.Context) error {
if f.loopIn.HtlcTxHash == nil && f.loopIn.HtlcOutputIndex == 0 &&
f.loopIn.HtlcOutputValue == 0 {
return nil
}
if f.cfg.Store == nil {
return errors.New("missing static address loop-in store")
}
f.loopIn.HtlcTxHash = nil
f.loopIn.HtlcOutputIndex = 0
f.loopIn.HtlcOutputValue = 0
return f.cfg.Store.UpdateLoopIn(ctx, f.loopIn)
}
// htlcTimeoutSweepRetryDelay is the delay between retries when publishing the
// htlc timeout sweep transaction fails.
const htlcTimeoutSweepRetryDelay = time.Hour

View file

@ -12,12 +12,14 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
@ -757,6 +759,7 @@ func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
t.Parallel()
mockLnd := test.NewMockLnd()
_, clientPubkey := test.CreateKey(20)
_, serverKey := test.CreateKey(21)
server := &mockStaticAddressServer{
@ -771,6 +774,10 @@ func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
Index: 0,
},
Value: 500_000,
AddressParams: &address.Parameters{
ClientPubkey: clientPubkey,
PkScript: []byte{0x51, 0x20, 0x01},
},
}
loopIn := &StaticAddressLoopIn{
@ -804,6 +811,17 @@ func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
require.Equal(t, OnHtlcInitiated, event)
require.Nil(t, f.LastActionError)
require.NotNil(t, server.request)
require.EqualValues(
t, swap.StaticAddressKeyFamily, loopIn.HtlcKeyLocator.Family,
)
require.Equal(
t, clientPubkey.SerializeCompressed(),
server.request.DepositToClientPubkeys[dep.String()].GetPubkey(),
)
require.Equal(
t, dep.AddressParams.PkScript,
server.request.DepositToClientPubkeys[dep.String()].GetPkScript(),
)
_, routeHints, _, _, err := swap.DecodeInvoice(
mockLnd.ChainParams, server.request.SwapInvoice,
@ -883,6 +901,7 @@ func TestCheckDepositsAvailableRejectsDivergentDepositOutpoints(
// update failure must not roll back the action or state transition.
func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
mockLnd := test.NewMockLnd()
_, depositClientPubkey := test.CreateKey(21)
_, serverKey := test.CreateKey(22)
server := &mockStaticAddressServer{
@ -897,6 +916,10 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
Index: 0,
},
Value: 500_000,
AddressParams: &address.Parameters{
ClientPubkey: depositClientPubkey,
PkScript: []byte{0x51, 0x20, 0x02},
},
}
loopIn := &StaticAddressLoopIn{
@ -917,6 +940,7 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
Server: server,
DepositManager: &noopDepositManager{},
LndClient: mockLnd.Client,
InvoicesClient: mockLnd.LndServices.Invoices,
WalletKit: mockLnd.WalletKit,
ChainParams: mockLnd.ChainParams,
Store: &mockStore{},
@ -940,6 +964,82 @@ func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) {
require.True(t, sendUpdateCalled)
}
// TestInitHtlcActionSendsChangeOutput asserts that fractional loop-ins create
// and send an operation-specific static change output to the server.
func TestInitHtlcActionSendsChangeOutput(t *testing.T) {
t.Parallel()
mockLnd := test.NewMockLnd()
_, depositClientPubkey := test.CreateKey(31)
_, changeClientPubkey := test.CreateKey(32)
_, serverKey := test.CreateKey(33)
server := &mockStaticAddressServer{
response: testStaticAddressLoopInResponse(
serverKey.SerializeCompressed(),
),
}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{3},
Index: 0,
},
Value: 500_000,
AddressParams: &address.Parameters{
ClientPubkey: depositClientPubkey,
PkScript: []byte{0x51, 0x20, 0x02},
},
}
changeParams := &address.Parameters{
ID: 1,
ClientPubkey: changeClientPubkey,
PkScript: []byte{0x51, 0x20, 0x01},
}
loopIn := &StaticAddressLoopIn{
Deposits: []*deposit.Deposit{dep},
DepositOutpoints: []string{dep.OutPoint.String()},
SelectedAmount: 300_000,
QuotedSwapFee: 1_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
PaymentTimeoutSeconds: 3_600,
}
f := &FSM{
StateMachine: &fsm.StateMachine{},
cfg: &Config{
Server: server,
AddressManager: &mockAddressManager{params: changeParams},
DepositManager: &noopDepositManager{},
LndClient: mockLnd.Client,
WalletKit: mockLnd.WalletKit,
ChainParams: mockLnd.ChainParams,
Store: &mockStore{},
ValidateLoopInContract: testValidateLoopInContract,
MaxStaticAddrHtlcFeePercentage: 1,
MaxStaticAddrHtlcBackupFeePercentage: 1,
},
loopIn: loopIn,
}
event := f.InitHtlcAction(t.Context(), nil)
require.Equal(t, OnHtlcInitiated, event)
require.Nil(t, f.LastActionError)
require.NotNil(t, server.request.ChangeOutput)
require.EqualValues(t, 200_000, server.request.ChangeOutput.Amount)
require.Equal(
t, changeClientPubkey.SerializeCompressed(),
server.request.ChangeOutput.StaticAddress.GetPubkey(),
)
require.Equal(
t, changeParams.PkScript,
server.request.ChangeOutput.StaticAddress.GetPkScript(),
)
require.Same(t, changeParams, loopIn.ChangeAddressParams)
}
// mockStaticAddressServer captures static-address loop-in requests in tests.
type mockStaticAddressServer struct {
swapserverrpc.StaticAddressServerClient
@ -978,6 +1078,70 @@ func testStaticAddressLoopInResponse(
}
}
type recordingLoopInStore struct {
mockStore
updates []*StaticAddressLoopIn
}
func (s *recordingLoopInStore) UpdateLoopIn(_ context.Context,
loopIn *StaticAddressLoopIn) error {
s.updates = append(s.updates, loopIn)
return nil
}
// TestRecordConfirmedHtlcPersistsOutpoint verifies that the FSM records the
// exact confirmed server HTLC output before the timeout branch can sweep it.
func TestRecordConfirmedHtlcPersistsOutpoint(t *testing.T) {
t.Parallel()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
loopIn := &StaticAddressLoopIn{
SwapHash: lntypes.Hash{1, 2, 4},
HtlcCltvExpiry: 800,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
}
htlc, err := loopIn.getHtlc(test.NewMockLnd().ChainParams)
require.NoError(t, err)
htlcValue := int64(123_456)
tx := wire.NewMsgTx(2)
tx.AddTxOut(&wire.TxOut{
Value: 1,
PkScript: []byte{0x51},
})
tx.AddTxOut(&wire.TxOut{
Value: htlcValue,
PkScript: htlc.PkScript,
})
store := &recordingLoopInStore{}
f := &FSM{
cfg: &Config{Store: store},
loopIn: loopIn,
}
err = f.recordConfirmedHtlc(
t.Context(), &chainntnfs.TxConfirmation{Tx: tx},
htlc.PkScript,
)
require.NoError(t, err)
txHash := tx.TxHash()
require.NotNil(t, loopIn.HtlcTxHash)
require.Equal(t, txHash, *loopIn.HtlcTxHash)
require.EqualValues(t, 1, loopIn.HtlcOutputIndex)
require.EqualValues(t, htlcValue, loopIn.HtlcOutputValue)
require.Len(t, store.updates, 1)
}
// testStaticAddressRouteHints returns deterministic route hints for static
// loop-in invoice regression tests.
func testStaticAddressRouteHints() [][]zpay32.HopHint {
@ -1086,10 +1250,18 @@ func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) {
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
NotificationManager: notificationMgr,
Store: &recordingLoopInStore{},
}
f, err := NewFSM(ctx, loopIn, cfg, false)
require.NoError(t, err)
htlc, err := loopIn.getHtlc(mockLnd.ChainParams)
require.NoError(t, err)
htlcTx := wire.NewMsgTx(2)
htlcTx.AddTxOut(&wire.TxOut{
Value: 1,
PkScript: htlc.PkScript,
})
resultChan := make(chan fsm.EventType, 1)
go func() {
@ -1108,7 +1280,7 @@ func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) {
case <-ctx.Done():
t.Fatalf("htlc conf registration not received: %v", ctx.Err())
}
confRegistration.ConfChan <- nil
confRegistration.ConfChan <- &chainntnfs.TxConfirmation{Tx: htlcTx}
select {
case hash := <-mockLnd.FailInvoiceChannel:
@ -2675,7 +2847,9 @@ func TestMonitorInvoiceAndHtlcTxDoesNotAdvanceWhenTimeoutDepositTransitionFails(
t.Fatalf("htlc conf registration not received: %v", ctx.Err())
}
confRegistration.ConfChan <- nil
confRegistration.ConfChan <- invoiceMonitorHtlcConfirmation(
t, f, mockLnd,
)
select {
case transition := <-depositMgr.transitionChan:
@ -2762,7 +2936,9 @@ func TestMonitorInvoiceAndHtlcTxRetriesOnlyPendingTimeoutDeposits(t *testing.T)
t.Fatalf("htlc conf registration not received: %v",
runCtx.Err())
}
confRegistration.ConfChan <- nil
confRegistration.ConfChan <- invoiceMonitorHtlcConfirmation(
t, f, mockLnd,
)
return resultChan
}
@ -2919,6 +3095,7 @@ func newInvoiceMonitorTestFSM(t *testing.T, ctx context.Context,
InvoicesClient: invoicesClient,
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
Store: &recordingLoopInStore{},
}
f, err := NewFSM(ctx, loopIn, cfg, true)
@ -2927,6 +3104,25 @@ func newInvoiceMonitorTestFSM(t *testing.T, ctx context.Context,
return f, depositMgr
}
// invoiceMonitorHtlcConfirmation returns a confirmation containing the HTLC
// output expected by the invoice monitor.
func invoiceMonitorHtlcConfirmation(t *testing.T, f *FSM,
mockLnd *test.LndMockServices) *chainntnfs.TxConfirmation {
t.Helper()
htlc, err := f.loopIn.getHtlc(mockLnd.ChainParams)
require.NoError(t, err)
htlcTx := wire.NewMsgTx(2)
htlcTx.AddTxOut(&wire.TxOut{
Value: int64(f.loopIn.TotalDepositAmount()),
PkScript: htlc.PkScript,
})
return &chainntnfs.TxConfirmation{Tx: htlcTx}
}
// failingCancelInvoices records cancellation attempts and returns a configured
// error after its release channel is closed.
type failingCancelInvoices struct {
@ -3142,10 +3338,15 @@ func TestInitHtlcActionCancelsInvoiceOnServerError(t *testing.T) {
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
loopIn := &StaticAddressLoopIn{
Deposits: []*deposit.Deposit{{
Value: 200_000,
AddressParams: &address.Parameters{
ClientPubkey: clientKey.PubKey(),
},
}},
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
@ -3192,12 +3393,17 @@ func TestInitHtlcActionCancelsInvoiceOnFeeGuardFailure(t *testing.T) {
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
loopIn := &StaticAddressLoopIn{
Deposits: []*deposit.Deposit{{
Value: 200_000,
AddressParams: &address.Parameters{
ClientPubkey: clientKey.PubKey(),
},
}},
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
@ -3341,6 +3547,13 @@ func (m *mockAddressManager) GetStaticAddress(_ context.Context) (
return nil, nil
}
// NewChangeAddress returns configured parameters for tests that need change.
func (m *mockAddressManager) NewChangeAddress(_ context.Context) (
*address.Parameters, error) {
return m.params, nil
}
// noopDepositManager is a stub DepositManager used to satisfy FSM config.
type noopDepositManager struct {
deposits []*deposit.Deposit

View file

@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swapserverrpc"
@ -41,6 +42,10 @@ type AddressManager interface {
// GetStaticAddress returns the deposit address for the given client and
// server public keys.
GetStaticAddress(ctx context.Context) (*script.StaticAddress, error)
// NewChangeAddress derives and persists a fresh static address from the
// change key family for this operation's change output.
NewChangeAddress(ctx context.Context) (*address.Parameters, error)
}
// DepositManager handles the interaction of loop-ins with deposits.

View file

@ -18,6 +18,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/staticutil"
@ -166,6 +167,11 @@ type StaticAddressLoopIn struct {
// Address is the address script that is used for the swap.
Address *script.StaticAddress
// ChangeAddressParams are the static address parameters for the change
// output that belongs to this swap. It is set only when SelectedAmount
// leaves non-dust change.
ChangeAddressParams *address.Parameters
// HTLC fields.
// HtlcTxFeeRate is the fee rate that is used for the htlc transaction.
@ -182,6 +188,16 @@ type StaticAddressLoopIn struct {
// HtlcTimeoutSweepTxHash is the hash of the htlc timeout sweep tx.
HtlcTimeoutSweepTxHash *chainhash.Hash
// HtlcTxHash is the hash of the confirmed htlc tx published by the
// server.
HtlcTxHash *chainhash.Hash
// HtlcOutputIndex is the output index of the confirmed htlc output.
HtlcOutputIndex uint32
// HtlcOutputValue is the value of the confirmed htlc output.
HtlcOutputValue btcutil.Amount
// HtlcTimeoutSweepAddress
HtlcTimeoutSweepAddress btcutil.Address
@ -204,19 +220,36 @@ func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
prevOuts, err := staticutil.ToPrevOuts(
l.Deposits, l.AddressParams.PkScript,
)
prevOuts, err := staticutil.ToPrevOuts(l.Deposits)
if err != nil {
return nil, err
}
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
outpoints := l.Outpoints()
if len(tx.TxIn) != len(outpoints) {
return nil, fmt.Errorf("htlc tx input count %d does not "+
"match deposits %d", len(tx.TxIn), len(outpoints))
}
if len(musig2sessions) != len(outpoints) {
return nil, fmt.Errorf("musig2 session count %d does not "+
"match deposits %d", len(musig2sessions), len(outpoints))
}
if len(counterPartyNonces) != len(outpoints) {
return nil, fmt.Errorf("server nonce count %d does not "+
"match deposits %d", len(counterPartyNonces),
len(outpoints))
}
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
sigs := make([][]byte, len(outpoints))
for idx, outpoint := range outpoints {
if musig2sessions[idx] == nil {
return nil, fmt.Errorf("missing musig2 session for "+
"deposit input %d", idx)
}
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
outpoint) {
@ -289,11 +322,10 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
// change.
var (
swapAmt = l.TotalDepositAmount()
changeAmount btcutil.Amount
changeAmount = l.ExpectedChangeAmount()
)
if l.SelectedAmount > 0 {
swapAmt = l.SelectedAmount
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
}
// Calculate htlc tx fee for server provided fee rate.
@ -329,9 +361,14 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
// We expect change to be sent back to our static address output script.
if changeAmount > 0 {
if l.ChangeAddressParams == nil {
return nil, fmt.Errorf("missing static address change " +
"parameters")
}
msgTx.AddTxOut(&wire.TxOut{
Value: int64(changeAmount),
PkScript: l.AddressParams.PkScript,
PkScript: l.ChangeAddressParams.PkScript,
})
}
@ -391,36 +428,18 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
return nil, err
}
htlcTx, err := l.createHtlcTx(
network, l.HtlcTxFeeRate, maxFeePercentage,
htlcOutpoint, htlcOutValue, err := l.confirmedHtlcOutpoint(
network, maxFeePercentage,
)
if err != nil {
return nil, err
}
// The HTLC output is always at index 0 (createHtlcTx adds it first).
// If there is a change output, it is at index 1. Verify this invariant
// so we fail fast if createHtlcTx's layout ever changes.
const htlcInputIndex = uint32(0)
if len(htlcTx.TxOut) == 2 {
if bytes.Equal(
htlcTx.TxOut[0].PkScript, l.AddressParams.PkScript,
) {
return nil, fmt.Errorf("htlc tx output layout " +
"invariant violated: expected HTLC output " +
"at index 0, got change output")
}
}
// Add the htlc input.
sweepTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: htlcInputIndex,
},
SignatureScript: htlc.SigScript,
Sequence: htlc.SuccessSequence(),
PreviousOutPoint: htlcOutpoint,
SignatureScript: htlc.SigScript,
Sequence: htlc.SuccessSequence(),
})
// Add the sweep output.
@ -431,7 +450,6 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
fee := feeRate.FeeForWeight(weightEstimator.Weight())
htlcOutValue := htlcTx.TxOut[htlcInputIndex].Value
output := &wire.TxOut{
Value: htlcOutValue - int64(fee),
PkScript: sweepPkScript,
@ -470,6 +488,56 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
return sweepTx, nil
}
// confirmedHtlcOutpoint returns the exact confirmed htlc outpoint when it has
// been persisted. Older loop-ins fall back to reconstructing the standard-fee
// htlc tx, which was the historical behavior before we stored the actual
// server-published variant.
func (l *StaticAddressLoopIn) confirmedHtlcOutpoint(
network *chaincfg.Params, maxFeePercentage float64) (wire.OutPoint,
int64, error) {
if l.HtlcTxHash != nil {
if l.HtlcOutputValue <= 0 {
return wire.OutPoint{}, 0, fmt.Errorf("missing htlc "+
"output value for confirmed htlc tx %v",
l.HtlcTxHash)
}
return wire.OutPoint{
Hash: *l.HtlcTxHash,
Index: l.HtlcOutputIndex,
}, int64(l.HtlcOutputValue), nil
}
htlcTx, err := l.createHtlcTx(
network, l.HtlcTxFeeRate, maxFeePercentage,
)
if err != nil {
return wire.OutPoint{}, 0, err
}
// The HTLC output is always at index 0 (createHtlcTx adds it first).
// If there is a change output, it is at index 1. Verify this invariant
// so we fail fast if createHtlcTx's layout ever changes.
const htlcInputIndex = uint32(0)
if len(htlcTx.TxOut) == 2 && l.ChangeAddressParams != nil {
if bytes.Equal(
htlcTx.TxOut[0].PkScript,
l.ChangeAddressParams.PkScript,
) {
return wire.OutPoint{}, 0, fmt.Errorf("htlc tx " +
"output layout invariant violated: expected " +
"HTLC output at index 0, got change output")
}
}
return wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: htlcInputIndex,
}, htlcTx.TxOut[htlcInputIndex].Value, nil
}
// pubkeyTo33ByteSlice converts a pubkey to a 33 byte slice.
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
var pubkeyBytes [33]byte
@ -491,6 +559,22 @@ func (l *StaticAddressLoopIn) TotalDepositAmount() btcutil.Amount {
return total
}
// ExpectedChangeAmount returns the change that a fractional loop-in should send
// to its generated static change address. A full-amount loop-in has no change.
func (l *StaticAddressLoopIn) ExpectedChangeAmount() btcutil.Amount {
if l.SelectedAmount <= 0 {
return 0
}
totalDepositAmount := l.TotalDepositAmount()
changeAmount := totalDepositAmount - l.SelectedAmount
if changeAmount <= 0 || changeAmount >= totalDepositAmount {
return 0
}
return changeAmount
}
// RemainingPaymentTimeSeconds returns the remaining time in seconds until the
// payment timeout is reached. The remaining time is calculated from the
// initiation time of the swap. If more than the swap's configured payment

View file

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/version"
@ -77,7 +78,8 @@ func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
Hash: chainhash.Hash{0xaa},
Index: 0,
},
Value: depositValue,
Value: depositValue,
AddressParams: addrParams,
},
}
@ -96,7 +98,7 @@ func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Deposits: deposits,
AddressParams: addrParams,
ChangeAddressParams: addrParams,
HtlcTxFeeRate: feeRate,
SelectedAmount: selectedAmount,
PaymentTimeoutSeconds: 3600,
@ -183,6 +185,76 @@ func TestPaymentTimeoutDuration(t *testing.T) {
}
}
// TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint verifies that timeout sweeps
// spend the actual server-published HTLC tx variant once it has been recorded.
func TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint(t *testing.T) {
t.Parallel()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
network := &chaincfg.RegressionNetParams
staticAddr, err := newStaticAddress(
clientKey.PubKey(), serverKey.PubKey(), 4032,
)
require.NoError(t, err)
pkScript, err := staticAddr.StaticAddressScript()
require.NoError(t, err)
addrParams := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PkScript: pkScript,
Expiry: 4032,
ProtocolVersion: version.ProtocolVersion_V0,
}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xbb},
Index: 0,
},
Value: 500_000,
AddressParams: addrParams,
}
confirmedHtlcHash := chainhash.Hash{0xcc}
confirmedHtlcValue := btcutil.Amount(275_000)
loopIn := &StaticAddressLoopIn{
SwapHash: lntypes.Hash{3, 2, 1},
HtlcCltvExpiry: 800,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Deposits: []*deposit.Deposit{dep},
HtlcTxFeeRate: chainfee.SatPerKWeight(253),
HtlcTxHash: &confirmedHtlcHash,
HtlcOutputIndex: 2,
HtlcOutputValue: confirmedHtlcValue,
}
sweepAddr, err := btcutil.NewAddressTaproot(make([]byte, 32), network)
require.NoError(t, err)
sweepTx, err := loopIn.createHtlcSweepTx(
t.Context(), &noopSigner{}, sweepAddr,
chainfee.SatPerKWeight(253), network,
uint32(loopIn.HtlcCltvExpiry)+1, 1,
)
require.NoError(t, err)
require.Len(t, sweepTx.TxIn, 1)
require.Equal(
t, wire.OutPoint{
Hash: confirmedHtlcHash,
Index: 2,
}, sweepTx.TxIn[0].PreviousOutPoint,
)
require.Less(t, sweepTx.TxOut[0].Value, int64(confirmedHtlcValue))
require.Greater(t, sweepTx.TxOut[0].Value, int64(0))
}
// newStaticAddress creates a StaticAddress for testing.
func newStaticAddress(clientKey, serverKey *btcec.PublicKey,
csvExpiry int64) (*script.StaticAddress, error) {

View file

@ -21,7 +21,6 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
@ -333,7 +332,7 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
// If the user selected an amount that is less than the total deposit
// amount we'll check that the server sends us the correct change amount
// back to our static address.
err = m.checkChange(ctx, sweepTx, loopIn.AddressParams)
err = m.checkChange(ctx, sweepTx)
if err != nil {
return err
}
@ -376,8 +375,18 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
map[string]*swapserverrpc.ClientSweeplessSigningInfo,
len(req.DepositToNonces),
)
depositMap := make(map[string]*deposit.Deposit, len(loopIn.Deposits))
for _, d := range loopIn.Deposits {
depositMap[d.String()] = d
}
for depositOutpoint, nonce := range req.DepositToNonces {
d, ok := depositMap[depositOutpoint]
if !ok {
return fmt.Errorf("deposit %v not found in loop-in",
depositOutpoint)
}
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault,
sweepPacket.UnsignedTx,
@ -396,7 +405,7 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
}
musig2Session, err := staticutil.CreateMusig2Session(
ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address,
ctx, m.cfg.Signer, d,
)
if err != nil {
return err
@ -461,7 +470,7 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
// swaps with identical change outputs. The client needs to ensure that any
// swap referenced by the inputs has a respective change output in the batch.
func (m *Manager) checkChange(ctx context.Context,
sweepTx *wire.MsgTx, changeAddr *script.Parameters) error {
sweepTx *wire.MsgTx) error {
prevOuts := make([]string, len(sweepTx.TxIn))
for i, in := range sweepTx.TxIn {
@ -486,42 +495,67 @@ func (m *Manager) checkChange(ctx context.Context,
return err
}
var expectedChange btcutil.Amount
var expectedChanges []*wire.TxOut
for swapHash := range swapHashes {
loopIn, err := m.cfg.Store.GetLoopInByHash(ctx, swapHash)
if err != nil {
return err
}
totalDepositAmount := loopIn.TotalDepositAmount()
changeAmt := totalDepositAmount - loopIn.SelectedAmount
if changeAmt > 0 && changeAmt < totalDepositAmount {
log.Debugf("expected change output to our "+
"static address, total_deposit_amount=%v, "+
"selected_amount=%v, "+
"expected_change_amount=%v ",
totalDepositAmount, loopIn.SelectedAmount,
changeAmt)
expectedChange += changeAmt
changeAmt := loopIn.ExpectedChangeAmount()
if changeAmt == 0 {
continue
}
if loopIn.ChangeAddressParams == nil {
return fmt.Errorf("missing change address for swap %x",
swapHash[:])
}
log.Debugf("expected change output to static address, "+
"swap_hash=%x, selected_amount=%v, "+
"expected_change_amount=%v", swapHash[:],
loopIn.SelectedAmount, changeAmt)
expectedChanges = append(expectedChanges, &wire.TxOut{
Value: int64(changeAmt),
PkScript: loopIn.ChangeAddressParams.PkScript,
})
}
if expectedChange == 0 {
if len(expectedChanges) == 0 {
return nil
}
for _, out := range sweepTx.TxOut {
if out.Value == int64(expectedChange) &&
bytes.Equal(out.PkScript, changeAddr.PkScript) {
// Match expected change outputs as a multiset. This rejects batched
// transactions that collapse two equal client change outputs into one
// output unless the protocol explicitly negotiates such aggregation.
matchedOutputs := make([]bool, len(sweepTx.TxOut))
for _, expected := range expectedChanges {
var found bool
for i, out := range sweepTx.TxOut {
if matchedOutputs[i] {
continue
}
// We found the expected change output.
return nil
if out.Value == expected.Value &&
bytes.Equal(out.PkScript, expected.PkScript) {
matchedOutputs[i] = true
found = true
break
}
}
if found {
continue
}
return fmt.Errorf("couldn't find expected change of %v "+
"satoshis sent to static address", expected.Value)
}
return fmt.Errorf("couldn't find expected change of %v "+
"satoshis sent to our static address", expectedChange)
return nil
}
// recover stars a loop-in state machine for each non-final loop-in to pick up
@ -668,19 +702,8 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
"deposits: %w", err)
}
// TODO(hieblmi): add params to deposit for multi-address
// support.
params, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, fmt.Errorf("unable to retrieve static "+
"address parameters: %w", err)
}
selectedDeposits, err = SelectDeposits(
req.SelectedAmount, allDeposits, params.Expiry,
m.currentHeight.Load(),
req.SelectedAmount, allDeposits, m.currentHeight.Load(),
)
if err != nil {
return nil, fmt.Errorf("unable to select deposits: %w",
@ -860,15 +883,21 @@ func (m *Manager) activeDepositsForLoopIn(loopIn *StaticAddressLoopIn) (
// leaving a dust change. It returns an error if the sum of deposits minus dust
// is less than the requested amount.
func SelectDeposits(targetAmount btcutil.Amount,
unfilteredDeposits []*deposit.Deposit, csvExpiry uint32,
blockHeight uint32) ([]*deposit.Deposit, error) {
unfilteredDeposits []*deposit.Deposit, blockHeight uint32) (
[]*deposit.Deposit, error) {
// Filter out deposits that are too close to expiry to be swapped.
var deposits []*deposit.Deposit
for _, d := range unfilteredDeposits {
confirmationHeight := d.GetConfirmationHeight()
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static address parameters "+
"for deposit %s", d.OutPoint.String())
}
if !IsSwappable(
uint32(confirmationHeight), blockHeight, csvExpiry,
uint32(confirmationHeight), blockHeight,
d.AddressParams.Expiry,
) {
log.Debugf("Skipping deposit %s as it expires before "+
@ -895,11 +924,11 @@ func SelectDeposits(targetAmount btcutil.Amount,
if deposits[i].Value == deposits[j].Value {
iExp := blocksUntilDepositExpiry(
uint32(iConfirmationHeight), blockHeight,
csvExpiry,
deposits[i].AddressParams.Expiry,
)
jExp := blocksUntilDepositExpiry(
uint32(jConfirmationHeight), blockHeight,
csvExpiry,
deposits[j].AddressParams.Expiry,
)
return iExp < jExp

View file

@ -14,6 +14,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
@ -193,9 +194,11 @@ func TestSelectDeposits(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setTestDepositParams(tc.deposits, tc.csvExpiry)
setTestDepositParams(tc.expected, tc.csvExpiry)
selectedDeposits, err := SelectDeposits(
tc.targetValue, tc.deposits, tc.csvExpiry,
tc.blockHeight,
tc.targetValue, tc.deposits, tc.blockHeight,
)
if tc.expectedErr == "" {
require.NoError(t, err)
@ -417,6 +420,14 @@ func TestGetAllSwapsPreservesStoreDeposits(t *testing.T) {
require.Equal(t, []*deposit.Deposit{currentDeposit}, swaps[0].Deposits)
}
func setTestDepositParams(deposits []*deposit.Deposit, expiry uint32) {
for _, d := range deposits {
d.AddressParams = &address.Parameters{
Expiry: expiry,
}
}
}
// TestIsSwappableUnconfirmed checks that an unconfirmed deposit is considered
// swappable because its CSV timeout has not started yet.
func TestIsSwappableUnconfirmed(t *testing.T) {
@ -655,9 +666,9 @@ func TestCheckChange(t *testing.T) {
var hash lntypes.Hash
hash[0] = h
li := &StaticAddressLoopIn{
Deposits: deposits,
SelectedAmount: selected,
AddressParams: changeAddr,
Deposits: deposits,
SelectedAmount: selected,
ChangeAddressParams: changeAddr,
}
return hash, li
}
@ -712,7 +723,6 @@ func TestCheckChange(t *testing.T) {
name string
inDeps []*deposit.Deposit // deposits referenced by tx inputs
outputs []*wire.TxOut // outputs in sweep tx
addr *script.Parameters
expectErr bool
expectedErrMsg string
}
@ -728,7 +738,6 @@ func TestCheckChange(t *testing.T) {
PkScript: serverAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "single swap change present",
@ -743,43 +752,59 @@ func TestCheckChange(t *testing.T) {
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "multiple swaps different change amounts",
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 900,
Value: 500,
PkScript: changeAddr.PkScript,
},
{
Value: 400,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "two swaps with identical change values sum correctly",
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
name: "two swaps with identical change values both present",
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 400,
PkScript: changeAddr.PkScript,
},
{
Value: 400,
PkScript: changeAddr.PkScript,
},
},
},
{
name: "collapsed identical change output rejected",
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)
outputs: []*wire.TxOut{
{
Value: 800,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
{
name: "missing change output results in error",
inDeps: []*deposit.Deposit{s2d1}, // expect 500
outputs: []*wire.TxOut{},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
@ -796,7 +821,6 @@ func TestCheckChange(t *testing.T) {
PkScript: otherAddr.PkScript,
},
},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
@ -813,7 +837,6 @@ func TestCheckChange(t *testing.T) {
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
@ -834,7 +857,6 @@ func TestCheckChange(t *testing.T) {
PkScript: otherAddr.PkScript,
},
},
addr: changeAddr,
},
}
@ -857,7 +879,7 @@ func TestCheckChange(t *testing.T) {
mgr.cfg.DepositManager = mdm
tx := makeSweepTx(inputs, tc.outputs)
err := mgr.checkChange(ctx, tx, tc.addr)
err := mgr.checkChange(ctx, tx)
if tc.expectErr {
require.Error(t, err)
if tc.expectedErrMsg != "" {

View file

@ -0,0 +1,71 @@
package loopin
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
// TestSignMusig2TxRejectsNonceCountMismatch verifies malformed server nonce
// sets fail cleanly instead of panicking when signing HTLC variants.
func TestSignMusig2TxRejectsNonceCountMismatch(t *testing.T) {
t.Parallel()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
network := &chaincfg.RegressionNetParams
staticAddr, err := newStaticAddress(
clientKey.PubKey(), serverKey.PubKey(), 4032,
)
require.NoError(t, err)
pkScript, err := staticAddr.StaticAddressScript()
require.NoError(t, err)
addrParams := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PkScript: pkScript,
Expiry: 4032,
ProtocolVersion: version.ProtocolVersion_V0,
}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xdd},
Index: 0,
},
Value: 500_000,
AddressParams: addrParams,
}
loopIn := &StaticAddressLoopIn{
SwapHash: lntypes.Hash{4, 5, 6},
HtlcCltvExpiry: 800,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Deposits: []*deposit.Deposit{dep},
HtlcTxFeeRate: chainfee.SatPerKWeight(253),
}
htlcTx, err := loopIn.createHtlcTx(network, loopIn.HtlcTxFeeRate, 1)
require.NoError(t, err)
_, err = loopIn.signMusig2Tx(
t.Context(), htlcTx, &noopSigner{},
[]*input.MuSig2SessionInfo{{}}, nil,
)
require.ErrorContains(t, err, "server nonce count")
}

View file

@ -14,6 +14,7 @@ import (
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/clock"
@ -294,6 +295,17 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds),
Fast: loopIn.Fast,
}
if loopIn.ChangeAddressParams != nil {
if loopIn.ChangeAddressParams.ID == 0 {
return errors.New("static address change parameters " +
"missing database ID")
}
staticAddressLoopInParams.ChangeStaticAddressID = sql.NullInt32{
Int32: loopIn.ChangeAddressParams.ID,
Valid: true,
}
}
updateTime := sqlStoreUpdateTime(s.clock)
updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{
@ -357,6 +369,11 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
htlcTimeoutSweepTxID = loopIn.HtlcTimeoutSweepTxHash.String()
}
var htlcTxID string
if loopIn.HtlcTxHash != nil {
htlcTxID = loopIn.HtlcTxHash.String()
}
updateParams := sqlc.UpdateStaticAddressLoopInParams{
SwapHash: loopIn.SwapHash[:],
HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate),
@ -364,6 +381,18 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
String: htlcTimeoutSweepTxID,
Valid: htlcTimeoutSweepTxID != "",
},
ConfirmedHtlcTxID: sql.NullString{
String: htlcTxID,
Valid: htlcTxID != "",
},
ConfirmedHtlcOutputIndex: sql.NullInt32{
Int32: int32(loopIn.HtlcOutputIndex),
Valid: htlcTxID != "",
},
ConfirmedHtlcOutputValue: sql.NullInt64{
Int64: int64(loopIn.HtlcOutputValue),
Valid: htlcTxID != "",
},
}
updateTime := sqlStoreUpdateTime(s.clock)
@ -575,6 +604,16 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
}
}
var htlcTxHash *chainhash.Hash
if swap.ConfirmedHtlcTxID.Valid {
htlcTxHash, err = chainhash.NewHashFromStr(
swap.ConfirmedHtlcTxID.String,
)
if err != nil {
return nil, err
}
}
var depositOutpoints []string
if swap.DepositOutpoints != "" {
depositOutpoints = strings.Split(
@ -601,7 +640,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
return nil, err
}
sqlcDeposit := sqlc.Deposit{
sqlcDeposit := sqlc.AllDepositsRow{
DepositID: id[:],
TxHash: d.TxHash,
Amount: d.Amount,
@ -610,6 +649,16 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
TimeoutSweepPkScript: d.TimeoutSweepPkScript,
ExpirySweepTxid: d.ExpirySweepTxid,
FinalizedWithdrawalTx: d.FinalizedWithdrawalTx,
SwapHash: d.SwapHash,
StaticAddressID: d.StaticAddressID,
ClientPubkey: d.ClientPubkey,
ServerPubkey: d.ServerPubkey,
Expiry: d.Expiry,
ClientKeyFamily: d.ClientKeyFamily,
ClientKeyIndex: d.ClientKeyIndex,
Pkscript: d.Pkscript,
ProtocolVersion: d.ProtocolVersion,
InitiationHeight: d.InitiationHeight,
}
sqlcDepositUpdate := sqlc.DepositUpdate{
@ -628,6 +677,11 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
}
depositList = orderDepositsBySnapshot(depositList, depositOutpoints)
changeAddressParams, err := toChangeAddressParameters(swap)
if err != nil {
return nil, err
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
SwapPreimage: swapPreImage,
@ -660,7 +714,13 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
),
HtlcTimeoutSweepAddress: timeoutAddress,
HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash,
Deposits: depositList,
HtlcTxHash: htlcTxHash,
HtlcOutputIndex: uint32(swap.ConfirmedHtlcOutputIndex.Int32),
HtlcOutputValue: btcutil.Amount(
swap.ConfirmedHtlcOutputValue.Int64,
),
Deposits: depositList,
ChangeAddressParams: changeAddressParams,
}
if swap.ConfirmationRiskDecisionTime.Valid {
loopIn.ConfirmationRiskDecisionTime =
@ -709,3 +769,41 @@ func orderDepositsBySnapshot(deposits []*deposit.Deposit,
return orderedDeposits
}
// toChangeAddressParameters converts the optional joined static address row
// into the change address parameters used to verify batched sweepless sweeps.
func toChangeAddressParameters(row sqlc.GetStaticAddressLoopInSwapRow) (
*address.Parameters, error) {
if !row.ChangeStaticAddressID.Valid {
return nil, nil
}
clientKey, err := btcec.ParsePubKey(row.ChangeClientPubkey)
if err != nil {
return nil, err
}
serverKey, err := btcec.ParsePubKey(row.ChangeServerPubkey)
if err != nil {
return nil, err
}
return &address.Parameters{
ID: row.ChangeStaticAddressID.Int32,
ClientPubkey: clientKey,
ServerPubkey: serverKey,
Expiry: uint32(row.ChangeExpiry.Int32),
PkScript: row.ChangePkscript,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(
row.ChangeClientKeyFamily.Int32,
),
Index: uint32(row.ChangeClientKeyIndex.Int32),
},
ProtocolVersion: version.AddressProtocolVersion(
row.ChangeProtocolVersion.Int32,
),
InitiationHeight: row.ChangeInitiationHeight.Int32,
}, nil
}

View file

@ -544,6 +544,70 @@ func TestGetLoopInByHashOrdersDepositsBySnapshot(t *testing.T) {
require.Equal(t, d1.ID, storedSwap.Deposits[1].ID)
}
func TestUpdateLoopInPersistsConfirmedHtlcOutpoint(t *testing.T) {
ctxb := context.Background()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
depositID, err := deposit.GetRandomDepositID()
require.NoError(t, err)
d := &deposit.Deposit{
ID: depositID,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
Index: 0,
},
Value: btcutil.Amount(100_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
},
}
require.NoError(t, depositStore.CreateDeposit(ctxb, d))
d.SetState(deposit.LoopingIn)
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
swapHash := lntypes.Hash{0x4, 0x2, 0x3, 0x5}
swap := StaticAddressLoopIn{
SwapHash: swapHash,
SwapPreimage: lntypes.Preimage{0x4, 0x2, 0x3, 0x5},
DepositOutpoints: []string{d.OutPoint.String()},
Deposits: []*deposit.Deposit{d},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swap.SetState(MonitorInvoiceAndHtlcTx)
require.NoError(t, swapStore.CreateLoopIn(ctxb, &swap))
confirmedHtlcTxHash := chainhash.Hash{0x55}
swap.HtlcTxHash = &confirmedHtlcTxHash
swap.HtlcOutputIndex = 2
swap.HtlcOutputValue = 88_000
require.NoError(t, swapStore.UpdateLoopIn(ctxb, &swap))
storedSwap, err := swapStore.GetLoopInByHash(ctxb, swapHash)
require.NoError(t, err)
require.NotNil(t, storedSwap.HtlcTxHash)
require.Equal(t, confirmedHtlcTxHash, *storedSwap.HtlcTxHash)
require.EqualValues(t, 2, storedSwap.HtlcOutputIndex)
require.EqualValues(t, 88_000, storedSwap.HtlcOutputValue)
require.Equal(t, MonitorInvoiceAndHtlcTx, storedSwap.GetState())
}
// TestGetLoopInByHashPreservesStoredDepositOutpoints ensures recovered loop-ins
// keep the original outpoint snapshot stored when the swap was created.
func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) {

View file

@ -9,6 +9,10 @@ import (
// Parameters holds all the necessary information for the 2-of-2 multisig
// address.
type Parameters struct {
// ID is the database primary key of the static address row. A zero value
// means the parameters have not been persisted yet.
ID int32
// ClientPubkey is the client's pubkey for the static address. It is
// used for the 2-of-2 funding output as well as for the client's
// timeout path.

View file

@ -3,6 +3,7 @@ package staticutil
import (
"bytes"
"context"
"errors"
"fmt"
"sort"
@ -11,8 +12,8 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
@ -21,8 +22,12 @@ import (
)
// ToPrevOuts converts a slice of deposits to a map of outpoints to TxOuts.
func ToPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
//
// Each deposit carries the static address parameters that produced its output.
// Using the per-deposit script here keeps signing correct when one transaction
// spends deposits from multiple static addresses.
func ToPrevOuts(deposits []*deposit.Deposit) (
map[wire.OutPoint]*wire.TxOut, error) {
outpoints := make([]wire.OutPoint, len(deposits))
for i, d := range deposits {
@ -35,9 +40,13 @@ func ToPrevOuts(deposits []*deposit.Deposit,
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for i, d := range deposits {
outpoint := outpoints[i]
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static address "+
"parameters for deposit %v", d.OutPoint)
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
PkScript: d.AddressParams.PkScript,
}
prevOuts[outpoint] = txOut
}
@ -45,11 +54,82 @@ func ToPrevOuts(deposits []*deposit.Deposit,
return prevOuts, nil
}
// DepositClientPubkeys maps each deposit outpoint to the static address
// descriptor that derives that output.
//
// The server receives this proof material with swap and withdrawal requests and
// verifies it against the L402's server key and expiry before co-signing any
// input.
func DepositClientPubkeys(deposits []*deposit.Deposit) (
map[string]*swapserverrpc.StaticAddressDescriptor, error) {
clientPubkeys := make(
map[string]*swapserverrpc.StaticAddressDescriptor, len(deposits),
)
for _, d := range deposits {
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static address "+
"parameters for deposit %v", d.OutPoint)
}
if d.AddressParams.ClientPubkey == nil {
return nil, fmt.Errorf("missing static address client "+
"pubkey for deposit %v", d.OutPoint)
}
if len(d.AddressParams.PkScript) == 0 {
return nil, fmt.Errorf("missing static address pkscript "+
"for deposit %v", d.OutPoint)
}
depositKey := d.String()
if _, ok := clientPubkeys[depositKey]; ok {
return nil, fmt.Errorf("duplicate outpoint %v",
depositKey)
}
clientPubkeys[depositKey] =
&swapserverrpc.StaticAddressDescriptor{
Pubkey: d.AddressParams.ClientPubkey.
SerializeCompressed(),
PkScript: d.AddressParams.PkScript,
}
}
return clientPubkeys, nil
}
// ChangeOutput converts a locally generated static address into the RPC change
// descriptor sent to the server. The descriptor binds the expected script,
// amount and client key so the server can derive and verify the same address.
func ChangeOutput(params *address.Parameters,
amount btcutil.Amount) (*swapserverrpc.StaticAddressChangeOutput, error) {
if amount <= 0 {
return nil, nil
}
if params == nil {
return nil, fmt.Errorf("missing static address change parameters")
}
if params.ClientPubkey == nil {
return nil, fmt.Errorf("missing static address change client " +
"pubkey")
}
if len(params.PkScript) == 0 {
return nil, fmt.Errorf("missing static address change pkscript")
}
return &swapserverrpc.StaticAddressChangeOutput{
StaticAddress: &swapserverrpc.StaticAddressDescriptor{
Pubkey: params.ClientPubkey.SerializeCompressed(),
PkScript: params.PkScript,
},
Amount: int64(amount),
}, nil
}
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
func CreateMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *script.Parameters,
staticAddress *script.StaticAddress) ([]*input.MuSig2SessionInfo,
signer lndclient.SignerClient, deposits []*deposit.Deposit) (
[]*input.MuSig2SessionInfo,
[][]byte, error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
@ -58,7 +138,7 @@ func CreateMusig2Sessions(ctx context.Context,
// Create the sessions and nonces from the deposits.
for i := range len(deposits) {
session, err := CreateMusig2Session(
ctx, signer, addrParams, staticAddress,
ctx, signer, deposits[i],
)
if err != nil {
return nil, nil, err
@ -72,11 +152,12 @@ func CreateMusig2Sessions(ctx context.Context,
}
// CreateMusig2SessionsPerDeposit creates a musig2 session for a number of
// deposits.
// deposits and returns the sessions keyed by outpoint string.
//
// The per-deposit keying mirrors the server response format and avoids relying
// on positional ordering after the request crosses the wire.
func CreateMusig2SessionsPerDeposit(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *script.Parameters,
staticAddress *script.StaticAddress) (
signer lndclient.SignerClient, deposits []*deposit.Deposit) (
map[string]*input.MuSig2SessionInfo, map[string][]byte, map[string]int,
error) {
@ -86,25 +167,73 @@ func CreateMusig2SessionsPerDeposit(ctx context.Context,
// Create the musig2 sessions for the sweepless sweep tx.
for i, deposit := range deposits {
session, err := CreateMusig2Session(
ctx, signer, addrParams, staticAddress,
)
if err != nil {
return nil, nil, nil, err
depositKey := deposit.String()
if _, ok := sessions[depositKey]; ok {
err := fmt.Errorf("duplicate outpoint %v", depositKey)
return nil, nil, nil, errors.Join(
err, CleanupMusig2Sessions(ctx, signer, sessions),
)
}
sessions[deposit.String()] = session
nonces[deposit.String()] = session.PublicNonce[:]
depositToIdx[deposit.String()] = i
session, err := CreateMusig2Session(
ctx, signer, deposit,
)
if err != nil {
return nil, nil, nil, errors.Join(
err, CleanupMusig2Sessions(ctx, signer, sessions),
)
}
sessions[depositKey] = session
nonces[depositKey] = session.PublicNonce[:]
depositToIdx[depositKey] = i
}
return sessions, nonces, depositToIdx, nil
}
// CreateMusig2Session creates a musig2 session for the deposit.
// CleanupMusig2Sessions releases all supplied MuSig2 sessions.
func CleanupMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient,
sessions map[string]*input.MuSig2SessionInfo) error {
var cleanupErr error
for depositKey, session := range sessions {
if session == nil {
continue
}
err := signer.MuSig2Cleanup(
context.WithoutCancel(ctx), session.SessionID,
)
if err != nil {
cleanupErr = errors.Join(
cleanupErr, fmt.Errorf("unable to clean up MuSig2 "+
"session for deposit %v: %w", depositKey, err),
)
}
}
return cleanupErr
}
// CreateMusig2Session creates a musig2 session for the deposit's static
// address.
func CreateMusig2Session(ctx context.Context,
signer lndclient.SignerClient, addrParams *script.Parameters,
staticAddress *script.StaticAddress) (*input.MuSig2SessionInfo, error) {
signer lndclient.SignerClient, d *deposit.Deposit) (
*input.MuSig2SessionInfo, error) {
if d.AddressParams == nil {
return nil, fmt.Errorf("missing static address parameters "+
"for deposit %v", d.OutPoint)
}
staticAddress, err := d.GetStaticAddressScript()
if err != nil {
return nil, err
}
addrParams := d.AddressParams
signers := [][]byte{
addrParams.ClientPubkey.SerializeCompressed(),

View file

@ -3,14 +3,16 @@ package staticutil
import (
"bytes"
"context"
"errors"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swapserverrpc"
looptest "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
@ -21,6 +23,37 @@ import (
"github.com/stretchr/testify/require"
)
type sessionCleanupSigner struct {
lndclient.SignerClient
createCalls int
failCreateAt int
cleaned [][32]byte
cleanupCtxErr []error
}
func (s *sessionCleanupSigner) MuSig2CreateSession(context.Context,
input.MuSig2Version, *keychain.KeyLocator, [][]byte,
...lndclient.MuSig2SessionOpts) (*input.MuSig2SessionInfo, error) {
s.createCalls++
if s.createCalls == s.failCreateAt {
return nil, errors.New("session creation failed")
}
sessionID := [32]byte{byte(s.createCalls)}
return &input.MuSig2SessionInfo{SessionID: sessionID}, nil
}
func (s *sessionCleanupSigner) MuSig2Cleanup(ctx context.Context,
sessionID [32]byte) error {
s.cleaned = append(s.cleaned, sessionID)
s.cleanupCtxErr = append(s.cleanupCtxErr, ctx.Err())
return nil
}
// mustHash converts a hex string to a chainhash.Hash and panics on error.
func mustHash(t *testing.T, s string) chainhash.Hash {
t.Helper()
@ -36,7 +69,8 @@ func TestToPrevOuts_Success(t *testing.T) {
Hash: mustHash(t, "0000000000000000000000000000000000000000000000000000000000000001"),
Index: 0,
},
Value: btcutil.Amount(12345),
Value: btcutil.Amount(12345),
AddressParams: &address.Parameters{PkScript: []byte{0x51}},
}
d2 := &deposit.Deposit{
@ -44,12 +78,11 @@ func TestToPrevOuts_Success(t *testing.T) {
Hash: mustHash(t, "1111111111111111111111111111111111111111111111111111111111111111"),
Index: 7,
},
Value: btcutil.Amount(987654321),
Value: btcutil.Amount(987654321),
AddressParams: &address.Parameters{PkScript: []byte{0x52}},
}
pkScript := []byte{0x51, 0x21, 0x02, 0x52} // arbitrary bytes
prevOuts, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, pkScript)
prevOuts, err := ToPrevOuts([]*deposit.Deposit{d1, d2})
require.NoError(t, err)
// We expect two entries.
@ -59,13 +92,13 @@ func TestToPrevOuts_Success(t *testing.T) {
txOut1, ok := prevOuts[d1.OutPoint]
require.True(t, ok, "expected outpoint d1 to be present")
require.EqualValues(t, int64(d1.Value), txOut1.Value)
require.Equal(t, pkScript, txOut1.PkScript)
require.Equal(t, d1.AddressParams.PkScript, txOut1.PkScript)
// Check the second outpoint mapping.
txOut2, ok := prevOuts[d2.OutPoint]
require.True(t, ok, "expected outpoint d2 to be present")
require.EqualValues(t, int64(d2.Value), txOut2.Value)
require.Equal(t, pkScript, txOut2.PkScript)
require.Equal(t, d2.AddressParams.PkScript, txOut2.PkScript)
// Ensure the keys in the map are exactly the outpoints we provided.
for op := range prevOuts {
@ -80,13 +113,169 @@ func TestToPrevOuts_DuplicateOutpoint(t *testing.T) {
Index: 2,
}
d1 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(100)}
d2 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(200)}
d1 := &deposit.Deposit{
OutPoint: shared,
Value: btcutil.Amount(100),
AddressParams: &address.Parameters{PkScript: []byte{0x00}},
}
d2 := &deposit.Deposit{
OutPoint: shared,
Value: btcutil.Amount(200),
AddressParams: &address.Parameters{PkScript: []byte{0x01}},
}
_, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, []byte{0x00})
_, err := ToPrevOuts([]*deposit.Deposit{d1, d2})
require.Error(t, err)
}
func TestToPrevOutsMissingAddressParams(t *testing.T) {
d := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "3333333333333333333333333333333333333333333333333333333333333333"),
Index: 3,
},
Value: btcutil.Amount(100),
}
_, err := ToPrevOuts([]*deposit.Deposit{d})
require.ErrorContains(t, err, "missing static address parameters")
}
func TestDepositClientPubkeys(t *testing.T) {
clientKey1, err := btcec.NewPrivateKey()
require.NoError(t, err)
clientKey2, err := btcec.NewPrivateKey()
require.NoError(t, err)
d1 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "4444444444444444444444444444444444444444444444444444444444444444"),
Index: 0,
},
AddressParams: &address.Parameters{
ClientPubkey: clientKey1.PubKey(),
PkScript: []byte{0x51, 0x20, 0x01},
},
}
d2 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "5555555555555555555555555555555555555555555555555555555555555555"),
Index: 1,
},
AddressParams: &address.Parameters{
ClientPubkey: clientKey2.PubKey(),
PkScript: []byte{0x51, 0x20, 0x02},
},
}
proofs, err := DepositClientPubkeys([]*deposit.Deposit{d1, d2})
require.NoError(t, err)
require.Equal(
t, clientKey1.PubKey().SerializeCompressed(),
proofs[d1.String()].GetPubkey(),
)
require.Equal(
t, d1.AddressParams.PkScript,
proofs[d1.String()].GetPkScript(),
)
require.Equal(
t, clientKey2.PubKey().SerializeCompressed(),
proofs[d2.String()].GetPubkey(),
)
require.Equal(
t, d2.AddressParams.PkScript,
proofs[d2.String()].GetPkScript(),
)
}
func TestDepositClientPubkeysRejectsInvalidDeposits(t *testing.T) {
t.Run("missing params", func(t *testing.T) {
d := &deposit.Deposit{OutPoint: wire.OutPoint{Index: 1}}
_, err := DepositClientPubkeys([]*deposit.Deposit{d})
require.ErrorContains(t, err, "missing static address parameters")
})
t.Run("missing client key", func(t *testing.T) {
d := &deposit.Deposit{
OutPoint: wire.OutPoint{Index: 1},
AddressParams: &address.Parameters{},
}
_, err := DepositClientPubkeys([]*deposit.Deposit{d})
require.ErrorContains(t, err, "missing static address client pubkey")
})
t.Run("duplicate outpoint", func(t *testing.T) {
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
d := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "6666666666666666666666666666666666666666666666666666666666666666"),
Index: 1,
},
AddressParams: &address.Parameters{
ClientPubkey: clientKey.PubKey(),
PkScript: []byte{0x51, 0x20, 0x03},
},
}
_, err = DepositClientPubkeys([]*deposit.Deposit{d, d})
require.ErrorContains(t, err, "duplicate outpoint")
})
t.Run("missing pkscript", func(t *testing.T) {
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
d := &deposit.Deposit{
OutPoint: wire.OutPoint{Index: 1},
AddressParams: &address.Parameters{
ClientPubkey: clientKey.PubKey(),
},
}
_, err = DepositClientPubkeys([]*deposit.Deposit{d})
require.ErrorContains(t, err, "missing static address pkscript")
})
}
func TestChangeOutput(t *testing.T) {
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
PkScript: []byte{0x51, 0x20, 0x01},
}
amount := btcutil.Amount(12345)
changeOutput, err := ChangeOutput(params, amount)
require.NoError(t, err)
require.Equal(
t, clientKey.PubKey().SerializeCompressed(),
changeOutput.StaticAddress.GetPubkey(),
)
require.Equal(t, params.PkScript, changeOutput.StaticAddress.GetPkScript())
require.EqualValues(t, amount, changeOutput.Amount)
changeOutput, err = ChangeOutput(params, 0)
require.NoError(t, err)
require.Nil(t, changeOutput)
}
func TestChangeOutputRejectsInvalidParams(t *testing.T) {
_, err := ChangeOutput(nil, 100)
require.ErrorContains(t, err, "missing static address change parameters")
_, err = ChangeOutput(&address.Parameters{}, 100)
require.ErrorContains(t, err, "missing static address change client pubkey")
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
_, err = ChangeOutput(&address.Parameters{
ClientPubkey: clientKey.PubKey(),
}, 100)
require.ErrorContains(t, err, "missing static address change pkscript")
}
func TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) {
// Helper to create a hash from string.
must := func(s string) chainhash.Hash {
@ -174,7 +363,7 @@ func TestCreateMusig2Session_Success(t *testing.T) {
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &script.Parameters{
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 10,
@ -182,13 +371,8 @@ func TestCreateMusig2Session_Success(t *testing.T) {
KeyLocator: keychain.KeyLocator{Family: 1, Index: 2},
}
// Build a static address for tweak options.
staticAddr, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey,
)
require.NoError(t, err)
sess, err := CreateMusig2Session(context.Background(), signer, params, staticAddr)
d := &deposit.Deposit{AddressParams: params}
sess, err := CreateMusig2Session(context.Background(), signer, d)
require.NoError(t, err)
require.NotNil(t, sess)
}
@ -203,7 +387,7 @@ func TestCreateMusig2Sessions_Multiple(t *testing.T) {
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &script.Parameters{
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 12,
@ -211,20 +395,15 @@ func TestCreateMusig2Sessions_Multiple(t *testing.T) {
KeyLocator: keychain.KeyLocator{Family: 9, Index: 8},
}
staticAddr, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey,
)
require.NoError(t, err)
// Prepare N deposits; only the length matters for session count.
deposits := []*deposit.Deposit{
{OutPoint: wire.OutPoint{Index: 0}},
{OutPoint: wire.OutPoint{Index: 1}},
{OutPoint: wire.OutPoint{Index: 2}},
{OutPoint: wire.OutPoint{Index: 0}, AddressParams: params},
{OutPoint: wire.OutPoint{Index: 1}, AddressParams: params},
{OutPoint: wire.OutPoint{Index: 2}, AddressParams: params},
}
sessions, nonces, err := CreateMusig2Sessions(
context.Background(), signer, deposits, params, staticAddr,
context.Background(), signer, deposits,
)
require.NoError(t, err)
require.Len(t, sessions, len(deposits))
@ -237,6 +416,43 @@ func TestCreateMusig2Sessions_Multiple(t *testing.T) {
}
}
func TestCreateMusig2SessionsPerDepositCleansUpPartialFailure(
t *testing.T) {
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 12,
KeyLocator: keychain.KeyLocator{Family: 9, Index: 8},
}
deposits := []*deposit.Deposit{
{
OutPoint: wire.OutPoint{Index: 1},
AddressParams: params,
},
{
OutPoint: wire.OutPoint{Index: 2},
AddressParams: params,
},
}
signer := &sessionCleanupSigner{failCreateAt: 2}
ctx, cancel := context.WithCancel(t.Context())
cancel()
_, _, _, err = CreateMusig2SessionsPerDeposit(
ctx, signer, deposits,
)
require.ErrorContains(t, err, "session creation failed")
require.Equal(t, [][32]byte{{1}}, signer.cleaned)
require.Equal(t, []error{nil}, signer.cleanupCtxErr)
}
// makeDeposit creates a deposit with the given value for testing.
func makeDeposit(value btcutil.Amount) *deposit.Deposit {
return &deposit.Deposit{Value: value}

View file

@ -5,6 +5,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
)
@ -18,6 +19,10 @@ type AddressManager interface {
// GetStaticAddress returns the deposit address for the given
// client and server public keys.
GetStaticAddress(ctx context.Context) (*script.StaticAddress, error)
// NewChangeAddress derives and persists a fresh static address from the
// change key family for this operation's change output.
NewChangeAddress(ctx context.Context) (*address.Parameters, error)
}
type DepositManager interface {

View file

@ -9,7 +9,6 @@ import (
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
@ -19,6 +18,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
@ -280,8 +280,7 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
}
err = m.handleWithdrawal(
ctx, deposits, tx.TxHash(),
tx.TxOut[0].PkScript,
ctx, deposits, tx.TxHash(), tx.TxOut[0].PkScript,
)
if err != nil {
return err
@ -536,40 +535,59 @@ func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
selectedWithdrawalAmount int64,
commitmentType lnrpc.CommitmentType) (*wire.MsgTx, []byte, error) {
// Create a musig2 session for each deposit.
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, nil, err
}
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return nil, nil, err
}
// Create a musig2 session for each deposit. Each selected deposit carries
// the address parameters that produced the output, so withdrawals can
// spend inputs from multiple static addresses in one transaction.
sessions, clientNonces, idx, err := staticutil.CreateMusig2SessionsPerDeposit(
ctx, m.cfg.Signer, deposits, addrParams, staticAddress,
ctx, m.cfg.Signer, deposits,
)
if err != nil {
return nil, nil, err
}
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, nil, fmt.Errorf("couldn't get confirmation "+
"height for deposit, %w", err)
}
defer func() {
err := staticutil.CleanupMusig2Sessions(
ctx, m.cfg.Signer, sessions,
)
if err != nil {
log.Warnf("Unable to clean up withdrawal MuSig2 "+
"sessions: %v", err)
}
}()
outpoints := toOutpoints(deposits)
prevOuts, err := staticutil.ToPrevOuts(deposits, params.PkScript)
prevOuts, err := staticutil.ToPrevOuts(deposits)
if err != nil {
return nil, nil, err
}
depositClientPubkeys, err := staticutil.DepositClientPubkeys(deposits)
if err != nil {
return nil, nil, fmt.Errorf("unable to prepare static address "+
"input proofs: %w", err)
}
_, changeAmount, err := CalculateWithdrawalTxValues(
deposits, btcutil.Amount(selectedWithdrawalAmount), feeRate,
withdrawalAddress, commitmentType,
)
if err != nil {
return nil, nil, fmt.Errorf("error calculating funding tx "+
"values: %w", err)
}
var changeParams *address.Parameters
if changeAmount > 0 {
changeParams, err = m.cfg.AddressManager.NewChangeAddress(ctx)
if err != nil {
return nil, nil, fmt.Errorf("unable to create static "+
"address change output: %w", err)
}
}
withdrawalTx, unsignedPsbt, err := m.createWithdrawalTx(
ctx, outpoints, deposits, prevOuts,
outpoints, deposits, prevOuts,
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
feeRate, commitmentType,
feeRate, commitmentType, changeParams,
)
if err != nil {
return nil, nil, err
@ -577,15 +595,16 @@ func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
// Request the server to sign the withdrawal transaction.
//
// The withdrawal and change amount are sent to the server with the
// expectation that the server just signs the transaction, without
// performing fee calculations and dust considerations. The client is
// responsible for that.
// All withdrawal outputs, including any change output, are encoded in
// the PSBT. The server signs the transaction as constructed without
// performing fee calculations or dust handling. The client is
// responsible for both.
// nolint:lll
sigResp, err := m.cfg.StaticAddressServerClient.ServerPsbtWithdrawDeposits(
ctx, &staticaddressrpc.ServerPsbtWithdrawRequest{
WithdrawalPsbt: unsignedPsbt,
DepositToNonces: clientNonces,
WithdrawalPsbt: unsignedPsbt,
DepositToNonces: clientNonces,
DepositToClientPubkeys: depositClientPubkeys,
},
)
if err != nil {
@ -664,22 +683,56 @@ func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
return true, nil
}
func withdrawalChangePkScript(tx *wire.MsgTx) []byte {
if tx == nil || len(tx.TxOut) < 2 {
return nil
}
return tx.TxOut[1].PkScript
}
// validateConfirmedWithdrawalInputs verifies that a confirmed withdrawal
// transaction spends every deposit associated with the withdrawal. Withdrawal
// monitoring intentionally watches only the first deposit so an RBF replacement
// can be discovered without registering a new confirmation notification for
// every replacement transaction. However, the spend notification also fires if
// an unrelated transaction spends only that first deposit. Requiring the full
// deposit set prevents such a partial spend from incorrectly transitioning all
// deposits to Withdrawn while still permitting a replacement transaction with a
// different transaction ID or additional inputs.
func validateConfirmedWithdrawalInputs(tx *wire.MsgTx,
deposits []*deposit.Deposit) error {
inputs := make(map[wire.OutPoint]struct{}, len(tx.TxIn))
for _, txIn := range tx.TxIn {
inputs[txIn.PreviousOutPoint] = struct{}{}
}
for _, d := range deposits {
if _, ok := inputs[d.OutPoint]; !ok {
return fmt.Errorf("confirmed transaction %v does not spend "+
"withdrawal deposit %v", tx.TxHash(), d.OutPoint)
}
}
return nil
}
// handleWithdrawal starts a goroutine that listens for the spent of the first
// input of the withdrawal transaction.
func (m *Manager) handleWithdrawal(ctx context.Context,
deposits []*deposit.Deposit, txHash chainhash.Hash,
withdrawalPkscript []byte) error {
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
log.Errorf("error retrieving address params: %v", err)
return fmt.Errorf("withdrawal failed")
}
deposits []*deposit.Deposit, originalTxHash chainhash.Hash,
withdrawalPkScript []byte) error {
d := deposits[0]
if d.AddressParams == nil {
return fmt.Errorf("missing static address parameters for %v",
d.OutPoint)
}
depositPkScript := d.AddressParams.PkScript
spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn(
ctx, &d.OutPoint, addrParams.PkScript,
ctx, &d.OutPoint, depositPkScript,
int32(d.GetConfirmationHeight()),
)
if err != nil {
@ -690,13 +743,20 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
select {
case spentTx := <-spentChan:
spendingHeight := uint32(spentTx.SpendingHeight)
spenderTxHash := originalTxHash
if spentTx.SpenderTxHash != nil {
spenderTxHash = *spentTx.SpenderTxHash
} else if spentTx.SpendingTx != nil {
spenderTxHash = spentTx.SpendingTx.TxHash()
}
// If the transaction received one confirmation, we
// ensure re-org safety by waiting for some more
// confirmations.
confChan, confErrChan, err :=
m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
ctx, spentTx.SpenderTxHash,
withdrawalPkscript, MinConfs,
ctx, &spenderTxHash, withdrawalPkScript,
MinConfs,
int32(m.initiationHeight.Load()),
)
if err != nil {
@ -710,6 +770,32 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
select {
case tx := <-confChan:
confirmedTx := spentTx.SpendingTx
if tx != nil && tx.Tx != nil {
confirmedTx = tx.Tx
}
if confirmedTx == nil {
log.Errorf("Confirmed withdrawal %v "+
"missing transaction",
spenderTxHash)
return
}
// Since the spend notification above only watches the
// first deposit, verify that the confirmed spender is the
// withdrawal (or one of its RBF replacements) before
// transitioning the complete deposit group.
err = validateConfirmedWithdrawalInputs(
confirmedTx, deposits,
)
if err != nil {
log.Errorf("Ignoring incomplete withdrawal: %v",
err)
return
}
err = m.cfg.DepositManager.TransitionDeposits(
ctx, deposits, deposit.OnWithdrawn,
deposit.Withdrawn,
@ -723,13 +809,14 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
// withdrawals to stop republishing it on block
// arrivals.
m.mu.Lock()
delete(m.finalizedWithdrawalTxns, txHash)
delete(m.finalizedWithdrawalTxns, originalTxHash)
delete(m.finalizedWithdrawalTxns, spenderTxHash)
m.mu.Unlock()
// Persist info about the finalized withdrawal.
err = m.cfg.Store.UpdateWithdrawal(
ctx, deposits, tx.Tx, spendingHeight,
addrParams.PkScript,
ctx, deposits, confirmedTx, spendingHeight,
withdrawalChangePkScript(confirmedTx),
)
if err != nil {
log.Errorf("Error persisting "+
@ -886,12 +973,13 @@ func (m *Manager) signMusig2Tx(ctx context.Context,
return tx, nil
}
func (m *Manager) createWithdrawalTx(ctx context.Context,
func (m *Manager) createWithdrawalTx(
outpoints []wire.OutPoint, deposits []*deposit.Deposit,
prevOuts map[wire.OutPoint]*wire.TxOut,
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
feeRate chainfee.SatPerKWeight,
commitmentType lnrpc.CommitmentType) (*wire.MsgTx, []byte, error) {
commitmentType lnrpc.CommitmentType,
changeParams *address.Parameters) (*wire.MsgTx, []byte, error) {
// First Create the tx.
msgTx := wire.NewMsgTx(2)
@ -938,30 +1026,14 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
})
if changeAmount > 0 {
// Send change back to the same static address.
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
log.Errorf("error retrieving taproot address %v", err)
return nil, nil, fmt.Errorf("withdrawal failed")
}
changeAddress, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(staticAddress.TaprootKey),
m.cfg.ChainParams,
)
if err != nil {
return nil, nil, err
}
changeScript, err := txscript.PayToAddrScript(changeAddress)
if err != nil {
return nil, nil, err
if changeParams == nil {
return nil, nil, fmt.Errorf("missing static address " +
"change parameters")
}
msgTx.AddTxOut(&wire.TxOut{
Value: int64(changeAmount),
PkScript: changeScript,
PkScript: changeParams.PkScript,
})
}

View file

@ -3,24 +3,54 @@ package withdraw
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
type withdrawalCleanupSigner struct {
lndclient.SignerClient
cleaned [][32]byte
cleanupCtxErr []error
}
func (s *withdrawalCleanupSigner) MuSig2CreateSession(context.Context,
input.MuSig2Version, *keychain.KeyLocator, [][]byte,
...lndclient.MuSig2SessionOpts) (*input.MuSig2SessionInfo, error) {
return &input.MuSig2SessionInfo{SessionID: [32]byte{1}}, nil
}
func (s *withdrawalCleanupSigner) MuSig2Cleanup(ctx context.Context,
sessionID [32]byte) error {
s.cleaned = append(s.cleaned, sessionID)
s.cleanupCtxErr = append(s.cleanupCtxErr, ctx.Err())
return nil
}
// TestNewManagerHeightValidation ensures the constructor rejects zero heights.
func TestNewManagerHeightValidation(t *testing.T) {
t.Parallel()
@ -35,6 +65,202 @@ func TestNewManagerHeightValidation(t *testing.T) {
require.NotNil(t, manager)
}
func TestWithdrawalChangePkScript(t *testing.T) {
t.Parallel()
require.Nil(t, withdrawalChangePkScript(nil))
tx := wire.NewMsgTx(2)
tx.AddTxOut(&wire.TxOut{
Value: 1000,
PkScript: []byte{0x01},
})
require.Nil(t, withdrawalChangePkScript(tx))
tx.AddTxOut(&wire.TxOut{
Value: 500,
PkScript: []byte{0x02},
})
require.Equal(t, []byte{0x02}, withdrawalChangePkScript(tx))
}
// TestValidateConfirmedWithdrawalInputs verifies that a replacement
// transaction must preserve the complete withdrawal deposit set. Additional
// inputs are allowed because they do not change which deposits are withdrawn.
func TestValidateConfirmedWithdrawalInputs(t *testing.T) {
t.Parallel()
first := &deposit.Deposit{
OutPoint: wire.OutPoint{Hash: chainhash.Hash{1}, Index: 1},
}
second := &deposit.Deposit{
OutPoint: wire.OutPoint{Hash: chainhash.Hash{2}, Index: 2},
}
deposits := []*deposit.Deposit{first, second}
partialSpend := wire.NewMsgTx(2)
partialSpend.AddTxIn(&wire.TxIn{
PreviousOutPoint: first.OutPoint,
})
err := validateConfirmedWithdrawalInputs(partialSpend, deposits)
require.ErrorContains(t, err, second.OutPoint.String())
replacement := wire.NewMsgTx(2)
replacement.AddTxIn(&wire.TxIn{
PreviousOutPoint: first.OutPoint,
})
replacement.AddTxIn(&wire.TxIn{
PreviousOutPoint: second.OutPoint,
})
replacement.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{3}, Index: 3,
},
})
require.NoError(
t, validateConfirmedWithdrawalInputs(replacement, deposits),
)
}
func TestCreateFinalizedWithdrawalTxCleansUpSessionsOnError(t *testing.T) {
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
signer := &withdrawalCleanupSigner{}
manager := &Manager{cfg: &ManagerConfig{Signer: signer}}
deposits := []*deposit.Deposit{
{
OutPoint: wire.OutPoint{Index: 1},
Value: 100_000,
AddressParams: &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 144,
PkScript: []byte{0x51},
KeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
},
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
_, _, err = manager.CreateFinalizedWithdrawalTx(
ctx, deposits, nil, 1_000, 0,
lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
)
require.ErrorContains(
t, err, "either address or commitment type must be specified",
)
require.Equal(t, [][32]byte{{1}}, signer.cleaned)
require.Equal(t, []error{nil}, signer.cleanupCtxErr)
}
type withdrawalConfRegistration struct {
txID *chainhash.Hash
pkScript []byte
numConfs int32
heightHint int32
}
type withdrawalTestNotifier struct {
lndclient.ChainNotifierClient
spendChan chan *chainntnfs.SpendDetail
spendErr chan error
confChan chan *chainntnfs.TxConfirmation
confErr chan error
confReq chan withdrawalConfRegistration
}
func newWithdrawalTestNotifier() *withdrawalTestNotifier {
return &withdrawalTestNotifier{
spendChan: make(chan *chainntnfs.SpendDetail, 1),
spendErr: make(chan error, 1),
confChan: make(chan *chainntnfs.TxConfirmation, 1),
confErr: make(chan error, 1),
confReq: make(chan withdrawalConfRegistration, 1),
}
}
func (n *withdrawalTestNotifier) RegisterSpendNtfn(context.Context,
*wire.OutPoint, []byte, int32, ...lndclient.NotifierOption) (
chan *chainntnfs.SpendDetail, chan error, error) {
return n.spendChan, n.spendErr, nil
}
func (n *withdrawalTestNotifier) RegisterConfirmationsNtfn(_ context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32,
_ ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation,
chan error, error) {
n.confReq <- withdrawalConfRegistration{
txID: txid,
pkScript: pkScript,
numConfs: numConfs,
heightHint: heightHint,
}
return n.confChan, n.confErr, nil
}
func TestHandleWithdrawalFollowsReplacementTxid(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
UseLogger(btclog.Disabled)
notifier := newWithdrawalTestNotifier()
manager, err := NewManager(&ManagerConfig{
ChainNotifier: notifier,
}, 123)
require.NoError(t, err)
originalTxHash := chainhash.Hash{1}
replacementTxHash := chainhash.Hash{2}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{3},
Index: 0,
},
ConfirmationHeight: 42,
AddressParams: &address.Parameters{
PkScript: []byte{0x51},
},
}
manager.finalizedWithdrawalTxns[originalTxHash] = wire.NewMsgTx(2)
withdrawalPkScript := []byte{0x51}
err = manager.handleWithdrawal(
ctx, []*deposit.Deposit{dep}, originalTxHash,
withdrawalPkScript,
)
require.NoError(t, err)
notifier.spendChan <- &chainntnfs.SpendDetail{
SpenderTxHash: &replacementTxHash,
SpendingTx: wire.NewMsgTx(2),
SpendingHeight: 50,
}
select {
case req := <-notifier.confReq:
require.NotNil(t, req.txID)
require.Equal(t, replacementTxHash, *req.txID)
require.Equal(t, withdrawalPkScript, req.pkScript)
require.Equal(t, MinConfs, req.numConfs)
require.EqualValues(t, 123, req.heightHint)
case <-ctx.Done():
t.Fatalf("confirmation registration not received: %v", ctx.Err())
}
}
// TestSignMusig2Tx_MissingSigningInfo tests that signMusig2Tx should error
// when sigInfo is missing an entry for one of the deposits.
//

View file

@ -5,7 +5,16 @@ var (
// spending of the htlc.
KeyFamily = int32(99)
// StaticAddressKeyFamily is the key family used to generate static
// address keys.
// StaticAddressKeyFamily is the legacy static-address key family. It is
// used for the V0 single static-address key and for static-address HTLC
// keys.
StaticAddressKeyFamily = int32(42060)
// StaticMultiAddressKeyFamily is the key family used to generate
// externally visible multi-address static-address receive keys.
StaticMultiAddressKeyFamily = int32(42061)
// StaticAddressChangeKeyFamily is the key family used to generate
// static-address change outputs.
StaticAddressChangeKeyFamily = int32(42062)
)

24
swap/keychain_test.go Normal file
View file

@ -0,0 +1,24 @@
package swap
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestStaticAddressKeyFamiliesAreDisjoint documents the key-family split used
// by static-address HTLC, receive and change key derivation.
func TestStaticAddressKeyFamiliesAreDisjoint(t *testing.T) {
families := map[int32]string{
KeyFamily: "swap htlc",
StaticAddressKeyFamily: "legacy static address and htlc",
StaticMultiAddressKeyFamily: "multi-address receive",
StaticAddressChangeKeyFamily: "static-address change",
}
require.Len(t, families, 4)
require.EqualValues(t, 99, KeyFamily)
require.EqualValues(t, 42060, StaticAddressKeyFamily)
require.EqualValues(t, 42061, StaticMultiAddressKeyFamily)
require.EqualValues(t, 42062, StaticAddressChangeKeyFamily)
}