mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/loopin: send per-deposit address proofs
Map every selected outpoint to the client key that derived its static address and include those proofs in loop-in requests. Keep MuSig2 signing indexed by outpoint so request ordering cannot select the wrong key.
This commit is contained in:
parent
689e1b119f
commit
b90c8fb386
6 changed files with 275 additions and 9 deletions
|
|
@ -158,16 +158,27 @@ 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,
|
||||
}
|
||||
if f.loopIn.LastHop != nil {
|
||||
loopInReq.LastHop = f.loopIn.LastHop
|
||||
|
|
|
|||
|
|
@ -757,6 +757,7 @@ func TestInitHtlcActionPreservesRouteHints(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
_, clientPubkey := test.CreateKey(20)
|
||||
_, serverKey := test.CreateKey(21)
|
||||
|
||||
server := &mockStaticAddressServer{
|
||||
|
|
@ -771,6 +772,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 +809,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,
|
||||
|
|
@ -3142,10 +3158,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 +3213,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(),
|
||||
|
|
|
|||
|
|
@ -211,10 +211,29 @@ func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
|
|||
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) {
|
||||
|
||||
|
|
|
|||
71
staticaddr/loopin/sign_musig_test.go
Normal file
71
staticaddr/loopin/sign_musig_test.go
Normal 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")
|
||||
}
|
||||
|
|
@ -53,6 +53,49 @@ 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
|
||||
}
|
||||
|
||||
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
|
||||
func CreateMusig2Sessions(ctx context.Context,
|
||||
signer lndclient.SignerClient, deposits []*deposit.Deposit) (
|
||||
|
|
|
|||
|
|
@ -141,6 +141,102 @@ func TestToPrevOutsMissingAddressParams(t *testing.T) {
|
|||
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 TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) {
|
||||
// Helper to create a hash from string.
|
||||
must := func(s string) chainhash.Hash {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue