Merge pull request #1043 from hieblmi/withdraw-psbt

staticaddr: psbt withdrawals
This commit is contained in:
Slyghtning 2025-12-09 13:30:15 +01:00 committed by GitHub
commit c3f3199625
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2000 additions and 613 deletions

View file

@ -76,6 +76,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
args.Error(1)
}
func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerPsbtWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
*swapserverrpc.ServerNewAddressResponse, error) {

View file

@ -93,6 +93,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
args.Error(1)
}
func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerPsbtWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
*swapserverrpc.ServerNewAddressResponse, error) {

View file

@ -17,6 +17,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
@ -318,8 +319,11 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
// Create a musig2 session for each deposit and different htlc tx fee
// rates.
createSession := f.loopIn.createMusig2Sessions
htlcSessions, clientHtlcNonces, err := createSession(ctx, f.cfg.Signer)
createSession := staticutil.CreateMusig2Sessions
htlcSessions, clientHtlcNonces, err := createSession(
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
err = fmt.Errorf("unable to create musig2 sessions: %w", err)
@ -328,7 +332,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessions)
htlcSessionsHighFee, highFeeNonces, err := createSession(
ctx, f.cfg.Signer,
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
return f.HandleError(err)
@ -336,7 +341,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessionsHighFee)
htlcSessionsExtremelyHighFee, extremelyHighNonces, err := createSession(
ctx, f.cfg.Signer,
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
err = fmt.Errorf("unable to convert nonces: %w", err)

View file

@ -21,6 +21,7 @@ import (
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
@ -169,47 +170,6 @@ func (l *StaticAddressLoopIn) getHtlc(chainParams *chaincfg.Params) (*swap.Htlc,
)
}
// createMusig2Sessions creates a musig2 session for a number of deposits.
func (l *StaticAddressLoopIn) createMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient) ([]*input.MuSig2SessionInfo, [][]byte,
error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(l.Deposits))
clientNonces := make([][]byte, len(l.Deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(l.Deposits); i++ {
session, err := l.createMusig2Session(ctx, signer)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// Musig2CreateSession creates a musig2 session for the deposit.
func (l *StaticAddressLoopIn) createMusig2Session(ctx context.Context,
signer lndclient.SignerClient) (*input.MuSig2SessionInfo, error) {
signers := [][]byte{
l.AddressParams.ClientPubkey.SerializeCompressed(),
l.AddressParams.ServerPubkey.SerializeCompressed(),
}
expiryLeaf := l.Address.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &l.AddressParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
// transaction.
func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
@ -217,7 +177,9 @@ func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
prevOuts, err := l.toPrevOuts(l.Deposits, l.AddressParams.PkScript)
prevOuts, err := staticutil.ToPrevOuts(
l.Deposits, l.AddressParams.PkScript,
)
if err != nil {
return nil, err
}
@ -523,29 +485,6 @@ func (l *StaticAddressLoopIn) Outpoints() []wire.OutPoint {
return outpoints
}
func (l *StaticAddressLoopIn) toPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
if _, ok := prevOuts[outpoint]; ok {
return nil, fmt.Errorf("duplicate outpoint %v",
outpoint)
}
prevOuts[outpoint] = txOut
}
return prevOuts, nil
}
// GetState returns the current state of the loop-in swap.
func (l *StaticAddressLoopIn) GetState() fsm.StateType {
l.mu.Lock()

View file

@ -21,6 +21,7 @@ import (
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
@ -391,8 +392,8 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
)
copy(serverNonce[:], nonce)
musig2Session, err := loopIn.createMusig2Session(
ctx, m.cfg.Signer,
musig2Session, err := staticutil.CreateMusig2Session(
ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address,
)
if err != nil {
return err

View file

@ -0,0 +1,25 @@
package staticutil
import (
"fmt"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc"
)
// ToWireOutpoints converts lnrpc.OutPoint protos into wire.OutPoint structs so
// they can be consumed by lower level transaction building code.
func ToWireOutpoints(outpoints []*lnrpc.OutPoint) ([]wire.OutPoint, error) {
serverOutpoints := make([]wire.OutPoint, 0, len(outpoints))
for _, o := range outpoints {
outpointStr := fmt.Sprintf("%s:%d", o.TxidStr, o.OutputIndex)
newOutpoint, err := wire.NewOutPointFromString(outpointStr)
if err != nil {
return nil, err
}
serverOutpoints = append(serverOutpoints, *newOutpoint)
}
return serverOutpoints, nil
}

View file

@ -0,0 +1,205 @@
package staticutil
import (
"bytes"
"context"
"fmt"
"sort"
"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"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
)
// 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) {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
if _, ok := prevOuts[outpoint]; ok {
return nil, fmt.Errorf("duplicate outpoint %v",
outpoint)
}
prevOuts[outpoint] = txOut
}
return prevOuts, nil
}
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
func CreateMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *address.Parameters,
staticAddress *script.StaticAddress) ([]*input.MuSig2SessionInfo,
[][]byte, error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
clientNonces := make([][]byte, len(deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(deposits); i++ {
session, err := CreateMusig2Session(
ctx, signer, addrParams, staticAddress,
)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// CreateMusig2SessionsPerDeposit creates a musig2 session for a number of
// deposits.
func CreateMusig2SessionsPerDeposit(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *address.Parameters,
staticAddress *script.StaticAddress) (
map[string]*input.MuSig2SessionInfo, map[string][]byte, map[string]int,
error) {
sessions := make(map[string]*input.MuSig2SessionInfo)
nonces := make(map[string][]byte)
depositToIdx := make(map[string]int)
// 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
}
sessions[deposit.String()] = session
nonces[deposit.String()] = session.PublicNonce[:]
depositToIdx[deposit.String()] = i
}
return sessions, nonces, depositToIdx, nil
}
// CreateMusig2Session creates a musig2 session for the deposit.
func CreateMusig2Session(ctx context.Context,
signer lndclient.SignerClient, addrParams *address.Parameters,
staticAddress *script.StaticAddress) (*input.MuSig2SessionInfo, error) {
signers := [][]byte{
addrParams.ClientPubkey.SerializeCompressed(),
addrParams.ServerPubkey.SerializeCompressed(),
}
expiryLeaf := staticAddress.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &addrParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
// GetPrevoutInfo converts a map of prevOuts to protobuf.
func GetPrevoutInfo(prevOuts map[wire.OutPoint]*wire.TxOut,
) []*swapserverrpc.PrevoutInfo {
prevoutInfos := make([]*swapserverrpc.PrevoutInfo, 0, len(prevOuts))
for outpoint, txOut := range prevOuts {
prevoutInfo := &swapserverrpc.PrevoutInfo{
TxidBytes: outpoint.Hash[:],
OutputIndex: outpoint.Index,
Value: uint64(txOut.Value),
PkScript: txOut.PkScript,
}
prevoutInfos = append(prevoutInfos, prevoutInfo)
}
// Sort UTXOs by txid:index using BIP-0069 rule. The function is used
// in unit tests a lot, and it is useful to make it deterministic.
sort.Slice(prevoutInfos, func(i, j int) bool {
return bip69inputLess(prevoutInfos[i], prevoutInfos[j])
})
return prevoutInfos
}
// bip69inputLess returns true if input1 < input2 according to BIP-0069
// First sort based on input hash (reversed / rpc-style), then index.
// The code is based on btcd/btcutil/txsort/txsort.go.
func bip69inputLess(input1, input2 *swapserverrpc.PrevoutInfo) bool {
// Input hashes are the same, so compare the index.
var ihash, jhash chainhash.Hash
copy(ihash[:], input1.TxidBytes)
copy(jhash[:], input2.TxidBytes)
if ihash == jhash {
return input1.OutputIndex < input2.OutputIndex
}
// At this point, the hashes are not equal, so reverse them to
// big-endian and return the result of the comparison.
const hashSize = chainhash.HashSize
for b := 0; b < hashSize/2; b++ {
ihash[b], ihash[hashSize-1-b] = ihash[hashSize-1-b], ihash[b]
jhash[b], jhash[hashSize-1-b] = jhash[hashSize-1-b], jhash[b]
}
return bytes.Compare(ihash[:], jhash[:]) == -1
}
// SelectDeposits sorts the deposits by amount in descending order. It then
// selects the deposits that are needed to cover the amount requested without
// leaving a dust change. It returns an error if the sum of deposits minus dust
// is less than the requested amount.
func SelectDeposits(deposits []*deposit.Deposit, amount int64) (
[]*deposit.Deposit, error) {
// Check that sum of deposits covers the swap amount while leaving no
// dust change.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
var depositSum btcutil.Amount
for _, deposit := range deposits {
depositSum += deposit.Value
}
if depositSum-dustLimit < btcutil.Amount(amount) {
return nil, fmt.Errorf("insufficient funds to cover swap " +
"amount, try manually selecting deposits")
}
// Sort the deposits by amount in descending order.
sort.Slice(deposits, func(i, j int) bool {
return deposits[i].Value > deposits[j].Value
})
// Select the deposits that are needed to cover the swap amount without
// leaving a dust change.
var selectedDeposits []*deposit.Deposit
var selectedAmount btcutil.Amount
for _, deposit := range deposits {
if selectedAmount >= btcutil.Amount(amount)+dustLimit {
break
}
selectedDeposits = append(selectedDeposits, deposit)
selectedAmount += deposit.Value
}
return selectedDeposits, nil
}

View file

@ -0,0 +1,236 @@
package staticutil
import (
"bytes"
"context"
"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/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"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/require"
)
// mustHash converts a hex string to a chainhash.Hash and panics on error.
func mustHash(t *testing.T, s string) chainhash.Hash {
t.Helper()
h, err := chainhash.NewHashFromStr(s)
require.NoError(t, err)
return *h
}
func TestToPrevOuts_Success(t *testing.T) {
// Prepare two distinct deposits with different outpoints and values.
d1 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "0000000000000000000000000000000000000000000000000000000000000001"),
Index: 0,
},
Value: btcutil.Amount(12345),
}
d2 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "1111111111111111111111111111111111111111111111111111111111111111"),
Index: 7,
},
Value: btcutil.Amount(987654321),
}
pkScript := []byte{0x51, 0x21, 0x02, 0x52} // arbitrary bytes
prevOuts, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, pkScript)
require.NoError(t, err)
// We expect two entries.
require.Len(t, prevOuts, 2)
// Check the first outpoint mapping.
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)
// 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)
// Ensure the keys in the map are exactly the outpoints we provided.
for op := range prevOuts {
require.True(t, op == d1.OutPoint || op == d2.OutPoint)
}
}
func TestToPrevOuts_DuplicateOutpoint(t *testing.T) {
// Two deposits that share the exact same outpoint should cause an error.
shared := wire.OutPoint{
Hash: mustHash(t, "2222222222222222222222222222222222222222222222222222222222222222"),
Index: 2,
}
d1 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(100)}
d2 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(200)}
_, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, []byte{0x00})
require.Error(t, err)
}
func TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) {
// Helper to create a hash from string.
must := func(s string) chainhash.Hash {
h, err := chainhash.NewHashFromStr(s)
require.NoError(t, err)
return *h
}
// Choose txids such that after reversal, ordering is determined by the
// last byte of the original hex string.
txidA := must("0000000000000000000000000000000000000000000000000000000000000001")
txidB := must("0000000000000000000000000000000000000000000000000000000000000002")
pkScript := []byte{0xaa, 0xbb}
prevOuts := map[wire.OutPoint]*wire.TxOut{
{Hash: txidA, Index: 5}: {Value: 11, PkScript: pkScript},
{Hash: txidA, Index: 2}: {Value: 22, PkScript: pkScript},
{Hash: txidB, Index: 0}: {Value: 33, PkScript: pkScript},
}
infos := GetPrevoutInfo(prevOuts)
// Expect deterministic ordering:
// 1) All entries with txidA (..01) before txidB (..02) due to BIP-69
// compare on reversed hashes.
// 2) Within txidA, index 2 before index 5.
require.Len(t, infos, 3)
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidA[:],
OutputIndex: 2,
Value: 22,
PkScript: pkScript,
}, infos[0])
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidA[:],
OutputIndex: 5,
Value: 11,
PkScript: pkScript,
}, infos[1])
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidB[:],
OutputIndex: 0,
Value: 33,
PkScript: pkScript,
}, infos[2])
}
func TestBip69InputLess_SameHashIndexOrder(t *testing.T) {
txid := make([]byte, 32)
txid[31] = 0x7f // Arbitrary value.
a := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 1}
b := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 3}
require.True(t, bip69inputLess(a, b))
require.False(t, bip69inputLess(b, a))
}
func TestBip69InputLess_DifferentHashes(t *testing.T) {
// txid1 ends with 0x01, txid2 ends with 0x02. After reversing for
// comparison, txid1 should still come before txid2 in lexicographic
// order.
h1, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001")
h2, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000002")
a := &swapserverrpc.PrevoutInfo{TxidBytes: h1[:], OutputIndex: 9}
b := &swapserverrpc.PrevoutInfo{TxidBytes: h2[:], OutputIndex: 0}
require.True(t, bip69inputLess(a, b))
require.False(t, bip69inputLess(b, a))
}
func TestCreateMusig2Session_Success(t *testing.T) {
// Set up mock signer from loop/test package.
lnd := looptest.NewMockLnd()
signer := lnd.Signer
// Create dummy key material for address parameters.
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: 10,
PkScript: []byte{0x51},
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)
require.NoError(t, err)
require.NotNil(t, sess)
}
func TestCreateMusig2Sessions_Multiple(t *testing.T) {
lnd := looptest.NewMockLnd()
signer := lnd.Signer
// Keys/params/static address.
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,
PkScript: []byte{0xaa},
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}},
}
sessions, nonces, err := CreateMusig2Sessions(
context.Background(), signer, deposits, params, staticAddr,
)
require.NoError(t, err)
require.Len(t, sessions, len(deposits))
require.Len(t, nonces, len(deposits))
// The mock signer returns a zero-value PublicNonce; assert consistency.
for i := range sessions {
require.NotNil(t, sessions[i])
require.True(t, bytes.Equal(nonces[i], sessions[i].PublicNonce[:]))
}
}

View file

@ -1,10 +1,10 @@
package withdraw
import (
"bytes"
"context"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"sync/atomic"
@ -12,6 +12,7 @@ import (
"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"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
@ -19,9 +20,12 @@ import (
"github.com/btcsuite/btcwallet/chain"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
@ -329,7 +333,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
// If not all passed outpoints are in state Deposited, we'll check if
// they are all in state Withdrawing. If they are, then the user is
// requesting a fee bump, if not we'll return an error as we only allow
// requesting a fee bump, if not, we'll return an error as we only allow
// fee bumping deposits in state Withdrawing.
if !allDeposited {
deposits, allWithdrawing = m.cfg.DepositManager.AllOutpointsActiveDeposits(
@ -406,7 +410,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
}
}
finalizedTx, err := m.createFinalizedWithdrawalTx(
finalizedTx, _, err := m.CreateFinalizedWithdrawalTx(
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
)
if err != nil {
@ -501,17 +505,30 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
return finalizedTx.TxID(), withdrawalAddress.String(), nil
}
func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
// CreateFinalizedWithdrawalTx creates and signs a finalized withdrawal
// transaction that can be broadcast to the network. It returns the
// signed *wire.MsgTx representation and the unsigned psbt.
func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx, []byte,
error) {
// Create a musig2 session for each deposit.
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
ctx, deposits,
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
}
sessions, clientNonces, idx, err := staticutil.CreateMusig2SessionsPerDeposit(
ctx, m.cfg.Signer, deposits, addrParams, staticAddress,
)
if err != nil {
return nil, err
return nil, nil, err
}
var withdrawalSweepFeeRate chainfee.SatPerKWeight
@ -521,7 +538,7 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
ctx, defaultConfTarget,
)
if err != nil {
return nil, err
return nil, nil, err
}
} else {
withdrawalSweepFeeRate = chainfee.SatPerKVByte(
@ -531,19 +548,23 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %w", err)
return nil, nil, fmt.Errorf("couldn't get confirmation "+
"height for deposit, %w", err)
}
outpoints := toOutpoints(deposits)
prevOuts := m.toPrevOuts(deposits, params.PkScript)
withdrawalTx, withdrawAmount, changeAmount, err := m.createWithdrawalTx(
ctx, outpoints, prevOuts,
prevOuts, err := staticutil.ToPrevOuts(deposits, params.PkScript)
if err != nil {
return nil, nil, err
}
withdrawalTx, unsignedPsbt, err := m.createWithdrawalTx(
ctx, outpoints, deposits, prevOuts,
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
withdrawalSweepFeeRate,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Request the server to sign the withdrawal transaction.
@ -552,45 +573,50 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
// expectation that the server just signs the transaction, without
// performing fee calculations and dust considerations. The client is
// responsible for that.
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
ctx, &staticaddressrpc.ServerWithdrawRequest{
Outpoints: toPrevoutInfo(outpoints),
ClientNonces: clientNonces,
ClientSweepAddr: withdrawalAddress.String(),
TxFeeRate: uint64(withdrawalSweepFeeRate),
WithdrawAmount: int64(withdrawAmount),
ChangeAmount: int64(changeAmount),
// nolint:lll
sigResp, err := m.cfg.StaticAddressServerClient.ServerPsbtWithdrawDeposits(
ctx, &staticaddressrpc.ServerPsbtWithdrawRequest{
WithdrawalPsbt: unsignedPsbt,
DepositToNonces: clientNonces,
},
)
if err != nil {
return nil, err
return nil, nil, err
}
coopServerNonces, err := toNonces(resp.ServerNonces)
if err != nil {
return nil, err
// Do some sanity checks.
txHash := withdrawalTx.TxHash()
if !bytes.Equal(txHash.CloneBytes(), sigResp.Txid) {
return nil, nil, errors.New("txid doesn't match")
}
if len(sigResp.SigningInfo) != len(deposits) {
return nil, nil, errors.New("invalid number of " +
"deposit signatures")
}
// Verify 1:1 matching between deposits and SigningInfo entries.
// Each deposit must have exactly one corresponding entry in
// SigningInfo.
for _, d := range deposits {
depositKey := d.OutPoint.String()
if _, ok := sigResp.SigningInfo[depositKey]; !ok {
return nil, nil, fmt.Errorf("missing signature for "+
"deposit %s", depositKey)
}
}
// Next we'll get our sweep tx signatures.
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
_, err = m.signMusig2Tx(
ctx, prevOutFetcher, outpoints, m.cfg.Signer, withdrawalTx,
withdrawalSessions, coopServerNonces,
finalizedTx, err := m.signMusig2Tx(
ctx, prevOutFetcher, m.cfg.Signer, withdrawalTx, sessions,
sigResp.SigningInfo, idx,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Now we'll finalize the sweepless sweep transaction.
finalizedTx, err := m.finalizeMusig2Transaction(
ctx, outpoints, m.cfg.Signer, withdrawalSessions,
withdrawalTx, resp.Musig2SweepSigs,
)
if err != nil {
return nil, err
}
return finalizedTx, nil
return finalizedTx, unsignedPsbt, nil
}
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
@ -726,105 +752,105 @@ func toOutpoints(deposits []*deposit.Deposit) []wire.OutPoint {
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
// transaction.
func (m *Manager) signMusig2Tx(ctx context.Context,
prevOutFetcher *txscript.MultiPrevOutFetcher, outpoints []wire.OutPoint,
prevOutFetcher *txscript.MultiPrevOutFetcher,
signer lndclient.SignerClient, tx *wire.MsgTx,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
sessions map[string]*input.MuSig2SessionInfo,
sigInfo map[string]*staticaddressrpc.ServerPsbtWithdrawSigningInfo,
depositsToIdx map[string]int) (*wire.MsgTx, error) {
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
sigs := make([][]byte, len(outpoints))
for idx, outpoint := range outpoints {
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
outpoint) {
// Create our digest.
var sigHash [32]byte
return nil, fmt.Errorf("tx input does not match " +
"deposits")
if len(sigInfo) != len(depositsToIdx) {
return nil, fmt.Errorf("unexpected number of partial " +
"signatures from server")
}
for txIndex, input := range tx.TxIn {
outpoint := input.PreviousOutPoint.String()
if i, ok := depositsToIdx[outpoint]; ok {
if i != txIndex {
return nil, fmt.Errorf("deposit index maps " +
"wrong tx index")
}
continue
}
return nil, fmt.Errorf("tx outpoint not in deposit index map")
}
// We'll now add the nonce to our session and sign the tx.
for deposit, sigAndNonce := range sigInfo {
session, ok := sessions[deposit]
if !ok {
return nil, errors.New("session not found")
}
nonce := [musig2.PubNonceSize]byte{}
copy(nonce[:], sigAndNonce.Nonce)
haveAllNonces, err := signer.MuSig2RegisterNonces(
ctx, session.SessionID,
[][musig2.PubNonceSize]byte{nonce},
)
if err != nil {
return nil, fmt.Errorf("error registering nonces: "+
"%w", err)
}
if !haveAllNonces {
return nil, errors.New("expected all nonces to be " +
"registered")
}
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault, tx, idx,
prevOutFetcher,
sigHashes, txscript.SigHashDefault, tx,
depositsToIdx[deposit], prevOutFetcher,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("error calculating taproot "+
"sig hash: %w", err)
}
var digest [32]byte
copy(digest[:], taprootSigHash)
copy(sigHash[:], taprootSigHash)
// Register the server's nonce before attempting to create our
// partial signature.
haveAllNonces, err := signer.MuSig2RegisterNonces(
ctx, musig2sessions[idx].SessionID,
[][musig2.PubNonceSize]byte{counterPartyNonces[idx]},
// Sign the tx.
_, err = signer.MuSig2Sign(
ctx, session.SessionID, sigHash, false,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("error signing tx: %w", err)
}
// Sanity check that we have all the nonces.
if !haveAllNonces {
return nil, fmt.Errorf("invalid MuSig2 session: " +
"nonces missing")
}
// Since our MuSig2 session has all nonces, we can now create
// the local partial signature by signing the sig hash.
sig, err := signer.MuSig2Sign(
ctx, musig2sessions[idx].SessionID, digest, false,
// Combine the signature with the client signature.
haveAllSigs, sig, err := signer.MuSig2CombineSig(
ctx, session.SessionID,
[][]byte{sigAndNonce.Sig},
)
if err != nil {
return nil, err
return nil, fmt.Errorf("error combining signature: "+
"%w", err)
}
sigs[idx] = sig
}
return sigs, nil
}
func withdrawalValue(prevOuts map[wire.OutPoint]*wire.TxOut) btcutil.Amount {
var totalValue btcutil.Amount
for _, prevOut := range prevOuts {
totalValue += btcutil.Amount(prevOut.Value)
}
return totalValue
}
// toNonces converts a byte slice to a 66 byte slice.
func toNonces(nonces [][]byte) ([][musig2.PubNonceSize]byte, error) {
res := make([][musig2.PubNonceSize]byte, 0, len(nonces))
for _, n := range nonces {
nonce, err := byteSliceTo66ByteSlice(n)
if err != nil {
return nil, err
if !haveAllSigs {
return nil, errors.New("expected all signatures to " +
"be combined")
}
res = append(res, nonce)
tx.TxIn[depositsToIdx[deposit]].Witness = wire.TxWitness{
sig,
}
}
return res, nil
}
// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
if len(b) != musig2.PubNonceSize {
return [musig2.PubNonceSize]byte{},
fmt.Errorf("invalid byte slice length")
}
var res [musig2.PubNonceSize]byte
copy(res[:], b)
return res, nil
return tx, nil
}
func (m *Manager) createWithdrawalTx(ctx context.Context,
outpoints []wire.OutPoint, prevOuts map[wire.OutPoint]*wire.TxOut,
outpoints []wire.OutPoint, deposits []*deposit.Deposit,
prevOuts map[wire.OutPoint]*wire.TxOut,
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, btcutil.Amount,
btcutil.Amount, error) {
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, []byte, error) {
// First Create the tx.
msgTx := wire.NewMsgTx(2)
@ -837,81 +863,23 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
})
}
var (
hasChange bool
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
withdrawalAmount btcutil.Amount
changeAmount btcutil.Amount
withdrawalAmount, changeAmount, err := CalculateWithdrawalTxValues(
deposits, selectedWithdrawalAmount, feeRate,
withdrawAddr, lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
)
// Estimate the transaction weight without change.
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
if err != nil {
return nil, 0, 0, err
}
feeWithoutChange := feeRate.FeeForWeightRoundUp(weight)
// If the user selected a fraction of the sum of the selected deposits
// to withdraw, check if a change output is needed.
totalWithdrawalAmount := withdrawalValue(prevOuts)
if selectedWithdrawalAmount > 0 {
// Estimate the transaction weight with change.
weight, err = withdrawalTxWeight(
len(outpoints), withdrawAddr, true,
)
if err != nil {
return nil, 0, 0, err
}
feeWithChange := feeRate.FeeForWeightRoundUp(weight)
// The available change that can cover fees is the total
// selected deposit amount minus the selected withdrawal amount.
change := totalWithdrawalAmount - selectedWithdrawalAmount
switch {
case change-feeWithChange >= dustLimit:
// If the change can cover the fees without turning into
// dust, add a non-dust change output.
hasChange = true
changeAmount = change - feeWithChange
withdrawalAmount = selectedWithdrawalAmount
case change-feeWithoutChange >= 0:
// If the change is dust, we give it to the miners.
hasChange = false
withdrawalAmount = selectedWithdrawalAmount
default:
// If the fees eat into our withdrawal amount, we fail
// the withdrawal.
return nil, 0, 0, fmt.Errorf("the change doesn't " +
"cover for fees. Consider lowering the fee " +
"rate or decrease the withdrawal amount")
}
} else {
// If the user wants to withdraw the full amount, we don't need
// a change output.
hasChange = false
withdrawalAmount = totalWithdrawalAmount - feeWithoutChange
return nil, nil, fmt.Errorf("error calculating funding tx "+
"values: %w", err)
}
if withdrawalAmount < dustLimit {
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
"dust limit")
}
if changeAmount < 0 {
return nil, 0, 0, fmt.Errorf("change amount is negative")
}
// For the users convenience we check that the change amount is lower
// For the user's convenience, we check that the change amount is lower
// than each input's value. If the change amount is higher than an
// input's value, we wouldn't have to include that input into the
// input's value, we wouldn't have to include that input in the
// transaction, saving fees.
for outpoint, txOut := range prevOuts {
if changeAmount >= btcutil.Amount(txOut.Value) {
return nil, 0, 0, fmt.Errorf("change amount %v is "+
"higher than an input value %v of input %v",
return nil, nil, fmt.Errorf("change amount %v "+
"is higher than an input value %v of input %v",
changeAmount, btcutil.Amount(txOut.Value),
outpoint)
}
@ -919,7 +887,7 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
if err != nil {
return nil, 0, 0, err
return nil, nil, err
}
// Create the withdrawal output.
@ -928,13 +896,13 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
PkScript: withdrawScript,
})
if hasChange {
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, 0, 0, fmt.Errorf("withdrawal failed")
return nil, nil, fmt.Errorf("withdrawal failed")
}
changeAddress, err := btcutil.NewAddressTaproot(
@ -942,12 +910,12 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
m.cfg.ChainParams,
)
if err != nil {
return nil, 0, 0, err
return nil, nil, err
}
changeScript, err := txscript.PayToAddrScript(changeAddress)
if err != nil {
return nil, 0, 0, err
return nil, nil, err
}
msgTx.AddTxOut(&wire.TxOut{
@ -956,11 +924,146 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
})
}
return msgTx, withdrawalAmount, changeAmount, nil
// Create psbt for the withdrawal.
psbtx, err := psbt.NewFromUnsignedTx(msgTx)
if err != nil {
return nil, nil, err
}
pInputs := make([]psbt.PInput, len(outpoints))
for i, op := range outpoints {
prevOut := prevOuts[op]
pInputs[i] = psbt.PInput{
WitnessUtxo: &wire.TxOut{
Value: prevOut.Value,
PkScript: prevOut.PkScript,
},
}
}
psbtx.Inputs = pInputs
// Serialize the psbt to send it to the client.
var psbtBuf bytes.Buffer
err = psbtx.Serialize(&psbtBuf)
if err != nil {
return nil, nil, err
}
return msgTx, psbtBuf.Bytes(), nil
}
// withdrawalFee returns the weight for the withdrawal transaction.
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
func CalculateWithdrawalTxValues(deposits []*deposit.Deposit,
localAmount btcutil.Amount, feeRate chainfee.SatPerKWeight,
withdrawalAddress btcutil.Address,
commitmentType lnrpc.CommitmentType) (btcutil.Amount, btcutil.Amount,
error) {
if withdrawalAddress == nil &&
commitmentType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
return 0, 0, fmt.Errorf("either address or commitment type " +
"must be specified")
}
var (
err error
withdrawalFundingAmt btcutil.Amount
changeAmount btcutil.Amount
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
isChannelOpen = commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE
)
totalDepositAmount := btcutil.Amount(0)
for _, d := range deposits {
totalDepositAmount += d.Value
}
// Estimate the open channel transaction fee without change.
hasChange := false
weight, err := WithdrawalTxWeight(
len(deposits), withdrawalAddress, commitmentType, hasChange,
)
if err != nil {
return 0, 0, err
}
feeWithoutChange := feeRate.FeeForWeight(weight)
// If the user selected a local amount for the channel, check if a
// change output is needed.
if localAmount > 0 {
// Estimate the transaction weight with change.
hasChange = true
weightWithChange, err := WithdrawalTxWeight(
len(deposits), withdrawalAddress, commitmentType,
hasChange,
)
if err != nil {
return 0, 0, err
}
feeWithChange := feeRate.FeeForWeight(weightWithChange)
// The available change that can cover fees is the total
// selected deposit amount minus the local channel amount.
change := totalDepositAmount - localAmount
switch {
case change-feeWithChange >= dustLimit:
// If the change can cover the fees without turning into
// dust, add a non-dust change output.
changeAmount = change - feeWithChange
withdrawalFundingAmt = localAmount
case change-feeWithoutChange >= 0:
// If the change is dust, we give it to the miners.
withdrawalFundingAmt = localAmount
default:
// If the fees eat into our local channel amount, we
// fail to open the channel.
return 0, 0, fmt.Errorf("the change doesn't " +
"cover for fees. Consider lowering the fee " +
"rate or decrease the local amount")
}
} else {
// If the user wants to open the channel with the total value of
// deposits, we don't need a change output.
withdrawalFundingAmt = totalDepositAmount - feeWithoutChange
}
if withdrawalFundingAmt < dustLimit {
return 0, 0, fmt.Errorf("withdrawal amount is below dust limit")
}
if changeAmount < 0 {
return 0, 0, fmt.Errorf("change amount is negative")
}
// Ensure that the channel funding amount is at least in the amount of
// lnd's minimum channel size.
if isChannelOpen && withdrawalFundingAmt < funding.MinChanFundingSize {
return 0, 0, fmt.Errorf("channel funding amount %v is lower "+
"than the minimum channel funding size %v",
withdrawalFundingAmt, funding.MinChanFundingSize)
}
// For the user's convenience, we check that the change amount is lower
// than each input's value. If the change amount is higher than an
// input's value, we wouldn't have to include that input in the
// transaction, saving fees.
for _, d := range deposits {
if changeAmount >= d.Value {
return 0, 0, fmt.Errorf("change amount %v is "+
"higher than an input value %v of input %v",
changeAmount, d.Value, d.OutPoint.String())
}
}
return withdrawalFundingAmt, changeAmount, nil
}
// WithdrawalTxWeight returns the weight for the withdrawal transaction.
func WithdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
commitmentType lnrpc.CommitmentType,
hasChange bool) (lntypes.WeightUnit, error) {
var weightEstimator input.TxWeightEstimator
@ -970,17 +1073,30 @@ func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
)
}
// Get the weight of the sweep output.
switch sweepAddress.(type) {
case *btcutil.AddressWitnessPubKeyHash:
weightEstimator.AddP2WKHOutput()
if commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
switch commitmentType {
case lnrpc.CommitmentType_SIMPLE_TAPROOT:
weightEstimator.AddP2TROutput()
case *btcutil.AddressTaproot:
weightEstimator.AddP2TROutput()
default:
weightEstimator.AddP2WSHOutput()
}
} else {
// Get the weight of the sweep output.
switch sweepAddress.(type) {
case *btcutil.AddressWitnessPubKeyHash:
weightEstimator.AddP2WKHOutput()
default:
return 0, fmt.Errorf("invalid sweep address type %T",
sweepAddress)
case *btcutil.AddressWitnessScriptHash:
weightEstimator.AddP2WSHOutput()
case *btcutil.AddressTaproot:
weightEstimator.AddP2TROutput()
default:
return 0, fmt.Errorf("invalid sweep address type %T",
sweepAddress)
}
}
// If there's a change output add the weight of the static address.
@ -991,120 +1107,6 @@ func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
return weightEstimator.Weight(), nil
}
// finalizeMusig2Transaction creates the finalized transactions for either
// the htlc or the cooperative close.
func (m *Manager) finalizeMusig2Transaction(ctx context.Context,
outpoints []wire.OutPoint, signer lndclient.SignerClient,
musig2Sessions []*input.MuSig2SessionInfo,
tx *wire.MsgTx, serverSigs [][]byte) (*wire.MsgTx, error) {
for idx := range outpoints {
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
ctx, musig2Sessions[idx].SessionID,
[][]byte{serverSigs[idx]},
)
if err != nil {
return nil, err
}
if !haveAllSigs {
return nil, fmt.Errorf("missing sigs")
}
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
}
return tx, nil
}
func toPrevoutInfo(outpoints []wire.OutPoint) []*staticaddressrpc.PrevoutInfo {
var result []*staticaddressrpc.PrevoutInfo
for _, o := range outpoints {
outP := o
outpoint := &staticaddressrpc.PrevoutInfo{
TxidBytes: outP.Hash[:],
OutputIndex: outP.Index,
}
result = append(result, outpoint)
}
return result
}
// createMusig2Sessions creates a musig2 session for a number of deposits.
func (m *Manager) createMusig2Sessions(ctx context.Context,
deposits []*deposit.Deposit) ([]*input.MuSig2SessionInfo, [][]byte,
error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
clientNonces := make([][]byte, len(deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(deposits); i++ {
session, err := m.createMusig2Session(ctx)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// Musig2CreateSession creates a musig2 session for the deposit.
func (m *Manager) createMusig2Session(ctx context.Context) (
*input.MuSig2SessionInfo, error) {
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %w", err)
}
signers := [][]byte{
addressParams.ClientPubkey.SerializeCompressed(),
addressParams.ServerPubkey.SerializeCompressed(),
}
address, err := m.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %w", err)
}
expiryLeaf := address.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return m.cfg.Signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &addressParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) map[wire.OutPoint]*wire.TxOut {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
prevOuts[outpoint] = txOut
}
return prevOuts
}
func (m *Manager) republishWithdrawals(ctx context.Context) error {
m.mu.Lock()
txns := make([]*wire.MsgTx, 0, len(m.finalizedWithdrawalTxns))

View file

@ -1,8 +1,22 @@
package withdraw
import (
"context"
"testing"
"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/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
@ -19,3 +33,576 @@ func TestNewManagerHeightValidation(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, manager)
}
// TestSignMusig2Tx_MissingSigningInfo tests that signMusig2Tx should error
// when sigInfo is missing an entry for one of the deposits.
//
// This test documents expected behavior. The function should validate that
// len(sigInfo) == len(sessions) and all sessions have corresponding sigInfo
// entries before attempting to sign, returning an error if validation fails.
func TestSignMusig2Tx_MissingSigningInfo(t *testing.T) {
t.Parallel()
// Create a dummy transaction with two inputs.
tx := wire.NewMsgTx(2)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{1},
Index: 0,
},
})
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{2},
Index: 0,
},
})
// Add a dummy output with a simple pkScript.
pkScript := []byte{
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
tx.AddTxOut(&wire.TxOut{
Value: 10000,
PkScript: pkScript,
})
// Create deposit keys for both inputs.
deposit1Key := "0100000000000000000000000000000000000000000000000000000000000000:0"
deposit2Key := "0200000000000000000000000000000000000000000000000000000000000000:0"
// Create sessions for both deposits.
sessions := map[string]*input.MuSig2SessionInfo{
deposit1Key: {
SessionID: [32]byte{1},
},
deposit2Key: {
SessionID: [32]byte{2},
},
}
// Create sigInfo with only one entry (missing deposit2).
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
deposit1Key: {
Nonce: make([]byte, 66),
Sig: make([]byte, 64),
},
}
// Create depositsToIdx mapping both deposits.
depositsToIdx := map[string]int{
deposit1Key: 0,
deposit2Key: 1,
}
// Create prevOutFetcher.
prevOuts := map[wire.OutPoint]*wire.TxOut{
tx.TxIn[0].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
tx.TxIn[1].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
}
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
// Create a mock signer.
lnd := test.NewMockLnd()
signer := lnd.Signer
// Create a minimal manager.
m := &Manager{
cfg: &ManagerConfig{
Signer: signer,
},
}
// Call signMusig2Tx - it should error because sigInfo is missing
// an entry for deposit2.
//
// The function should validate that:
// 1. len(sigInfo) == len(sessions) == len(tx.TxIn)
// 2. All keys in sessions exist in sigInfo
// 3. No partial signing is allowed
//
// This test verifies that the function errors when sigInfo is
// incomplete, preventing a partially signed transaction.
ctx := context.Background()
_, err := m.signMusig2Tx(
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
depositsToIdx,
)
// Expect an error. The function should validate that sigInfo has
// entries for all sessions before attempting to sign.
require.ErrorContains(t, err, "unexpected number of partial "+
"signatures from server")
}
// TestSignMusig2Tx_MismatchedIndex tests that signMusig2Tx should error when
// sigInfo has all expected keys but one maps to the wrong index in depositsToIdx.
//
// This test documents expected behavior. The function should validate that
// each deposit maps to a unique index in [0, len(tx.TxIn)-1] before signing,
// returning an error if indices conflict or are out of bounds.
func TestSignMusig2Tx_MismatchedIndex(t *testing.T) {
t.Parallel()
// Create a dummy transaction with two inputs.
tx := wire.NewMsgTx(2)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{1},
Index: 0,
},
})
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{2},
Index: 0,
},
})
// Add a dummy output with a simple pkScript.
pkScript := []byte{
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
tx.AddTxOut(&wire.TxOut{
Value: 10000,
PkScript: pkScript,
})
// Create deposit keys for both inputs.
deposit1Key := "0000000000000000000000000000000000000000000000000000000000000001:0"
deposit2Key := "0000000000000000000000000000000000000000000000000000000000000002:0"
// Create sessions for both deposits.
sessions := map[string]*input.MuSig2SessionInfo{
deposit1Key: {
SessionID: [32]byte{1},
},
deposit2Key: {
SessionID: [32]byte{2},
},
}
// Create sigInfo with both entries.
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
deposit1Key: {
Nonce: make([]byte, 66),
Sig: make([]byte, 64),
},
deposit2Key: {
Nonce: make([]byte, 66),
Sig: make([]byte, 64),
},
}
// Create depositsToIdx with WRONG mapping for deposit2.
// deposit2 should map to index 1, but we map it to 0.
depositsToIdx := map[string]int{
deposit1Key: 0,
deposit2Key: 0, // Wrong! Should be 1.
}
// Create prevOutFetcher.
prevOuts := map[wire.OutPoint]*wire.TxOut{
tx.TxIn[0].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
tx.TxIn[1].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
}
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
// Create a mock signer.
lnd := test.NewMockLnd()
signer := lnd.Signer
// Create a minimal manager.
m := &Manager{
cfg: &ManagerConfig{
Signer: signer,
},
}
// Call signMusig2Tx - it should error because depositsToIdx has
// a mismatched index for deposit2.
//
// The function should validate that:
// 1. Each deposit in depositsToIdx maps to a unique index
// 2. All indices from 0 to len(tx.TxIn)-1 are covered exactly once
// 3. No index is used twice (which would cause signature overwrites)
//
// In this case, both deposit1 and deposit2 map to index 0, which
// is invalid. The second deposit would overwrite the witness of
// the first input, resulting in an invalid transaction.
ctx := context.Background()
_, err := m.signMusig2Tx(
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
depositsToIdx,
)
// Expect an error. The function should validate index uniqueness.
require.ErrorContains(t, err, "deposit index maps wrong tx index")
}
// TestSignMusig2Tx_MissingOutpointInDepositMap tests that signMusig2Tx errors
// when a transaction input's outpoint is not present in depositsToIdx map.
//
// This test validates that the function checks all transaction inputs have
// corresponding entries in the depositsToIdx map before signing.
func TestSignMusig2Tx_MissingOutpointInDepositMap(t *testing.T) {
t.Parallel()
// Create a dummy transaction with two inputs.
tx := wire.NewMsgTx(2)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{1},
Index: 0,
},
})
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{2},
Index: 0,
},
})
// Add a dummy output with a simple pkScript.
pkScript := []byte{
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
tx.AddTxOut(&wire.TxOut{
Value: 10000,
PkScript: pkScript,
})
// Create deposit keys for both inputs.
deposit1Key := "0100000000000000000000000000000000000000000000000000000000000000:0"
deposit2Key := "0200000000000000000000000000000000000000000000000000000000000000:0"
// Create sessions for both deposits.
sessions := map[string]*input.MuSig2SessionInfo{
deposit1Key: {
SessionID: [32]byte{1},
},
deposit2Key: {
SessionID: [32]byte{2},
},
}
// Create sigInfo with both entries.
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
deposit1Key: {
Nonce: make([]byte, 66),
Sig: make([]byte, 64),
},
deposit2Key: {
Nonce: make([]byte, 66),
Sig: make([]byte, 64),
},
}
// Create a third deposit key that doesn't correspond to any tx input.
deposit3Key := "0300000000000000000000000000000000000000000000000000000000000000:0"
// Create depositsToIdx with deposit1 at correct index, but deposit3
// (which doesn't exist as a tx input) instead of deposit2.
// This means when the function iterates through tx.TxIn, it will find
// deposit1 in the map, but deposit2 (the actual second input) won't
// be found.
depositsToIdx := map[string]int{
deposit1Key: 0,
deposit3Key: 1, // Wrong key - this isn't a real tx input
}
// Create prevOutFetcher.
prevOuts := map[wire.OutPoint]*wire.TxOut{
tx.TxIn[0].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
tx.TxIn[1].PreviousOutPoint: {
Value: 5000,
PkScript: pkScript,
},
}
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
// Create a mock signer.
lnd := test.NewMockLnd()
signer := lnd.Signer
// Create a minimal manager.
m := &Manager{
cfg: &ManagerConfig{
Signer: signer,
},
}
// Call signMusig2Tx - it should error because the second transaction
// input's outpoint is not in depositsToIdx map.
//
// The function should validate that every transaction input has a
// corresponding entry in depositsToIdx before attempting to sign.
ctx := context.Background()
_, err := m.signMusig2Tx(
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
depositsToIdx,
)
// Expect an error indicating the missing outpoint.
require.ErrorContains(t, err, "tx outpoint not in deposit index map")
}
// TestCalculateWithdrawalTxValues tests various edge cases in withdrawal
// transaction value calculations.
func TestCalculateWithdrawalTxValues(t *testing.T) {
t.Parallel()
// Create a taproot address for withdrawal.
taprootAddr, err := btcutil.NewAddressTaproot(
make([]byte, 32), &chaincfg.RegressionNetParams,
)
require.NoError(t, err)
// Standard fee rate for testing.
feeRate := chainfee.SatPerKWeight(1000)
// Helper to create deposits.
createDeposit := func(value btcutil.Amount, idx uint32) *deposit.Deposit {
hash := chainhash.Hash{}
hash[0] = byte(idx)
return &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: hash,
Index: idx,
},
Value: value,
}
}
tests := []struct {
name string
deposits []*deposit.Deposit
localAmount btcutil.Amount
feeRate chainfee.SatPerKWeight
withdrawAddr btcutil.Address
commitmentType lnrpc.CommitmentType
expectedErr string
expectDustFee bool // change is dust, given to miners
}{
{
name: "neither address nor commitment type specified",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "either address or commitment type must be specified",
},
{
name: "change is dust - given to miners",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
// Set localAmount such that change after feeWithChange
// would be dust, but change after feeWithoutChange >= 0.
// This triggers case: change-feeWithoutChange >= 0
localAmount: 99300, // Leaves ~700 sats which is dust
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
expectDustFee: true,
},
{
name: "insufficient funds after dust and fee",
deposits: []*deposit.Deposit{
createDeposit(1000, 0),
},
localAmount: 900,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "doesn't cover for fees",
},
{
name: "negative change after fees",
deposits: []*deposit.Deposit{
createDeposit(10000, 0),
},
localAmount: 15000,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "doesn't cover for fees",
},
{
name: "min channel size guard - below minimum",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize-10, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "is lower than the minimum channel " +
"funding size",
},
{
name: "min channel size guard - exactly minimum",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize+1000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "",
},
{
name: "withdrawal amount below dust limit",
deposits: []*deposit.Deposit{
createDeposit(400, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "below dust limit",
},
{
name: "change higher than input value",
deposits: []*deposit.Deposit{
createDeposit(10000, 0),
createDeposit(5000, 1),
},
localAmount: 5000,
feeRate: chainfee.SatPerKWeight(100),
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "change amount",
},
{
name: "successful withdrawal with change",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 50000,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
},
{
name: "successful withdrawal no change",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
},
{
name: "successful channel open above min size",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize*2, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
withdrawAmt, changeAmt, err := CalculateWithdrawalTxValues(
tc.deposits, tc.localAmount, tc.feeRate,
tc.withdrawAddr, tc.commitmentType,
)
if tc.expectedErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedErr)
return
}
require.NoError(t, err)
require.Greater(t, withdrawAmt, btcutil.Amount(0))
require.GreaterOrEqual(t, changeAmt, btcutil.Amount(0))
// Verify that withdrawal amount meets dust threshold.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
require.GreaterOrEqual(t, withdrawAmt, dustLimit)
// If this is a channel open, verify min channel size.
if tc.commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
require.GreaterOrEqual(
t, withdrawAmt, funding.MinChanFundingSize,
)
}
// If expecting dust to be given to miners, verify
// changeAmt is 0.
if tc.expectDustFee {
require.Equal(t, btcutil.Amount(0), changeAmt,
"change should be 0 when dust is given to miners")
}
// Verify total accounting: inputs = withdrawal + change + fees.
totalInputs := btcutil.Amount(0)
for _, d := range tc.deposits {
totalInputs += d.Value
}
hasChange := changeAmt > 0
weight, err := WithdrawalTxWeight(
len(tc.deposits), tc.withdrawAddr,
tc.commitmentType, hasChange,
)
require.NoError(t, err)
fee := tc.feeRate.FeeForWeight(weight)
// When dust is given to miners, the "fee" includes both
// the transaction fee and the dust amount.
if tc.expectDustFee {
// Total should equal withdrawal + implicit fee (including dust)
implicitFee := totalInputs - withdrawAmt - changeAmt
require.Greater(t, implicitFee, fee,
"implicit fee should be greater than tx fee when dust is given to miners")
} else {
require.Equal(t, totalInputs, withdrawAmt+changeAmt+fee)
}
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,20 @@ service StaticAddressServer {
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. The server will generate
// the partial sigs for the client's selected deposits.
//
// Deprecated: use ServerPsbtWithdrawDeposits instead.
rpc ServerWithdrawDeposits (ServerWithdrawRequest)
returns (ServerWithdrawResponse);
returns (ServerWithdrawResponse) {
option deprecated = true;
};
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. In contrast to
// ServerWithdrawDeposits which provides the paramters to form the
// withdrawal transaction, this service method will provide a psbt which
// is signed by the server.
rpc ServerPsbtWithdrawDeposits (ServerPsbtWithdrawRequest)
returns (ServerPsbtWithdrawResponse);
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
// server will respond with htlc details that the client can use to
@ -67,7 +79,10 @@ message ServerAddressParameters {
uint32 expiry = 2;
}
// Deprecated: use ServerPsbtWithdrawRequest instead.
message ServerWithdrawRequest {
option deprecated = true;
// The deposit outpoints the client wishes to withdraw.
repeated PrevoutInfo outpoints = 1;
@ -91,14 +106,43 @@ message ServerWithdrawRequest {
int64 change_amount = 6;
}
// Deprecated: use ServerPsbtWithdrawResponse instead.
message ServerWithdrawResponse {
// The sweep sigs that the server generated for the htlc.
option deprecated = true;
// The sweep sigs that the server generated for the withdrawal tx.
repeated bytes musig2_sweep_sigs = 1;
// The nonces that the server used to generate the sweepless sweep sigs.
// The nonces that the server used to generate the withdrawal sigs.
repeated bytes server_nonces = 2;
}
message ServerPsbtWithdrawRequest {
// The withdrawal psbt.
// Note that txscript.SigHashDefault will be enforced by default.
bytes withdrawal_psbt = 1;
// The map of deposit txid:idx to the nonce used by the client.
map<string, bytes> deposit_to_nonces = 2;
}
message ServerPsbtWithdrawResponse {
// The txid of the psbt that the client wants to push the sigs for.
bytes txid = 1;
// A map of deposits in format txid:idx to the nonces.
map<string, ServerPsbtWithdrawSigningInfo> signing_info = 2;
}
message ServerPsbtWithdrawSigningInfo {
// The nonces that the client used to generate the partial withdrawal tx
// sigs.
bytes nonce = 1;
// The musig2 htlc sigs that the client generated for the withdrawal tx.
bytes sig = 2;
}
message ServerStaticAddressLoopInRequest {
// The client's public key for the htlc output.
bytes htlc_client_pub_key = 1;

View file

@ -22,10 +22,19 @@ type StaticAddressServerClient interface {
// The server will generate the address and return the server key and the
// address's CSV expiry.
ServerNewAddress(ctx context.Context, in *ServerNewAddressRequest, opts ...grpc.CallOption) (*ServerNewAddressResponse, error)
// Deprecated: Do not use.
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. The server will generate
// the partial sigs for the client's selected deposits.
//
// Deprecated: use ServerPsbtWithdrawDeposits instead.
ServerWithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error)
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. In contrast to
// ServerWithdrawDeposits which provides the paramters to form the
// withdrawal transaction, this service method will provide a psbt which
// is signed by the server.
ServerPsbtWithdrawDeposits(ctx context.Context, in *ServerPsbtWithdrawRequest, opts ...grpc.CallOption) (*ServerPsbtWithdrawResponse, error)
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
// server will respond with htlc details that the client can use to
// construct and sign the htlc tx.
@ -54,6 +63,7 @@ func (c *staticAddressServerClient) ServerNewAddress(ctx context.Context, in *Se
return out, nil
}
// Deprecated: Do not use.
func (c *staticAddressServerClient) ServerWithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error) {
out := new(ServerWithdrawResponse)
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerWithdrawDeposits", in, out, opts...)
@ -63,6 +73,15 @@ func (c *staticAddressServerClient) ServerWithdrawDeposits(ctx context.Context,
return out, nil
}
func (c *staticAddressServerClient) ServerPsbtWithdrawDeposits(ctx context.Context, in *ServerPsbtWithdrawRequest, opts ...grpc.CallOption) (*ServerPsbtWithdrawResponse, error) {
out := new(ServerPsbtWithdrawResponse)
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerPsbtWithdrawDeposits", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *staticAddressServerClient) ServerStaticAddressLoopIn(ctx context.Context, in *ServerStaticAddressLoopInRequest, opts ...grpc.CallOption) (*ServerStaticAddressLoopInResponse, error) {
out := new(ServerStaticAddressLoopInResponse)
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerStaticAddressLoopIn", in, out, opts...)
@ -98,10 +117,19 @@ type StaticAddressServerServer interface {
// The server will generate the address and return the server key and the
// address's CSV expiry.
ServerNewAddress(context.Context, *ServerNewAddressRequest) (*ServerNewAddressResponse, error)
// Deprecated: Do not use.
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. The server will generate
// the partial sigs for the client's selected deposits.
//
// Deprecated: use ServerPsbtWithdrawDeposits instead.
ServerWithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error)
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
// haven't timed out yet to the client's wallet. In contrast to
// ServerWithdrawDeposits which provides the paramters to form the
// withdrawal transaction, this service method will provide a psbt which
// is signed by the server.
ServerPsbtWithdrawDeposits(context.Context, *ServerPsbtWithdrawRequest) (*ServerPsbtWithdrawResponse, error)
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
// server will respond with htlc details that the client can use to
// construct and sign the htlc tx.
@ -124,6 +152,9 @@ func (UnimplementedStaticAddressServerServer) ServerNewAddress(context.Context,
func (UnimplementedStaticAddressServerServer) ServerWithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ServerWithdrawDeposits not implemented")
}
func (UnimplementedStaticAddressServerServer) ServerPsbtWithdrawDeposits(context.Context, *ServerPsbtWithdrawRequest) (*ServerPsbtWithdrawResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ServerPsbtWithdrawDeposits not implemented")
}
func (UnimplementedStaticAddressServerServer) ServerStaticAddressLoopIn(context.Context, *ServerStaticAddressLoopInRequest) (*ServerStaticAddressLoopInResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ServerStaticAddressLoopIn not implemented")
}
@ -182,6 +213,24 @@ func _StaticAddressServer_ServerWithdrawDeposits_Handler(srv interface{}, ctx co
return interceptor(ctx, in, info, handler)
}
func _StaticAddressServer_ServerPsbtWithdrawDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerPsbtWithdrawRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StaticAddressServerServer).ServerPsbtWithdrawDeposits(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.StaticAddressServer/ServerPsbtWithdrawDeposits",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StaticAddressServerServer).ServerPsbtWithdrawDeposits(ctx, req.(*ServerPsbtWithdrawRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StaticAddressServer_ServerStaticAddressLoopIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerStaticAddressLoopInRequest)
if err := dec(in); err != nil {
@ -251,6 +300,10 @@ var StaticAddressServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "ServerWithdrawDeposits",
Handler: _StaticAddressServer_ServerWithdrawDeposits_Handler,
},
{
MethodName: "ServerPsbtWithdrawDeposits",
Handler: _StaticAddressServer_ServerPsbtWithdrawDeposits_Handler,
},
{
MethodName: "ServerStaticAddressLoopIn",
Handler: _StaticAddressServer_ServerStaticAddressLoopIn_Handler,