Merge pull request #547 from bhandras/musig-keyreveal

loop: Loop In MuSig2 support
pull/560/head
András Bánki-Horváth 1 year ago committed by GitHub
commit 7f19c43047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,6 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
@ -240,8 +239,12 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
Preimage: preimage,
AmountRequested: amt,
CltvExpiry: 744,
ReceiverKey: receiverKey,
SenderKey: senderKey,
HtlcKeys: loopdb.HtlcKeys{
SenderScriptKey: senderKey,
SenderInternalPubKey: senderKey,
ReceiverScriptKey: receiverKey,
ReceiverInternalPubKey: receiverKey,
},
MaxSwapFee: 60000,
MaxMinerFee: 50000,
ProtocolVersion: protocolVersion,
@ -273,30 +276,13 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
// Expect client to register for our expected number of confirmations.
confIntent := ctx.AssertRegisterConf(preimageRevealed, int32(confs))
// Assert that the loopout htlc equals to the expected one.
scriptVersion := GetHtlcScriptVersion(protocolVersion)
var htlc *swap.Htlc
switch scriptVersion {
case swap.HtlcV2:
htlc, err = swap.NewHtlcV2(
pendingSwap.Contract.CltvExpiry, senderKey,
receiverKey, hash, &chaincfg.TestNet3Params,
)
case swap.HtlcV3:
htlc, err = swap.NewHtlcV3(
input.MuSig2Version040,
pendingSwap.Contract.CltvExpiry, senderKey,
receiverKey, senderKey, receiverKey, hash,
&chaincfg.TestNet3Params,
)
default:
t.Fatalf(swap.ErrInvalidScriptVersion.Error())
}
htlc, err := GetHtlc(
hash, &pendingSwap.Contract.SwapContract,
&chaincfg.TestNet3Params,
)
require.NoError(t, err)
// Assert that the loopout htlc equals to the expected one.
require.Equal(t, htlc.PkScript, confIntent.PkScript)
signalSwapPaymentResult(nil)
@ -315,7 +301,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
func(r error) {},
func(r error) {},
preimageRevealed,
confIntent, scriptVersion,
confIntent, GetHtlcScriptVersion(protocolVersion),
)
}

@ -10,6 +10,31 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
)
// HtlcKeys is a holder of all keys used when constructing the swap HTLC. Since
// it's used for both loop in and loop out swaps it may hold partial information
// about the sender or receiver depending on the swap type.
type HtlcKeys struct {
// SenderScriptKey is the sender's public key that is used in the HTLC,
// specifically when constructing the script spend scripts.
SenderScriptKey [33]byte
// SenderInternalPubKey is the sender's internal pubkey that is used in
// taproot HTLCs as part of the aggregate internal key.
SenderInternalPubKey [33]byte
// ReceiverScriptKey is the receiver's public key that is used in the
// HTLC, specifically when constructing the script spend scripts.
ReceiverScriptKey [33]byte
// ReceiverInternalPubKey is the sender's internal pubkey that is used
// in taproot HTLCs as part of the aggregate internal key.
ReceiverInternalPubKey [33]byte
// ClientScriptKeyLocator is the client's key locator for the key used
// in the HTLC script spend scripts.
ClientScriptKeyLocator keychain.KeyLocator
}
// SwapContract contains the base data that is serialized to persistent storage
// for pending swaps.
type SwapContract struct {
@ -19,18 +44,8 @@ type SwapContract struct {
// AmountRequested is the total amount of the swap.
AmountRequested btcutil.Amount
// SenderKey is the key of the sender that will be used in the on-chain
// HTLC.
SenderKey [33]byte
// ReceiverKey is the of the receiver that will be used in the on-chain
// HTLC.
ReceiverKey [33]byte
// ClientKeyLocator is the key locator (family and index) for the client
// key. It is for the receiver key if this is a loop out contract, or
// the sender key if this is a loop in contract.
ClientKeyLocator keychain.KeyLocator
// HtlcKeys holds all keys used in the swap HTLC construction.
HtlcKeys HtlcKeys
// CltvExpiry is the total absolute CLTV expiry of the swap.
CltvExpiry int32

@ -63,7 +63,7 @@ func serializeLoopInContract(swap *LoopInContract) (
return nil, err
}
n, err := b.Write(swap.SenderKey[:])
n, err := b.Write(swap.HtlcKeys.SenderScriptKey[:])
if err != nil {
return nil, err
}
@ -71,7 +71,7 @@ func serializeLoopInContract(swap *LoopInContract) (
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = b.Write(swap.ReceiverKey[:])
n, err = b.Write(swap.HtlcKeys.ReceiverScriptKey[:])
if err != nil {
return nil, err
}
@ -161,7 +161,7 @@ func deserializeLoopInContract(value []byte) (*LoopInContract, error) {
return nil, err
}
n, err := r.Read(contract.SenderKey[:])
n, err := r.Read(contract.HtlcKeys.SenderScriptKey[:])
if err != nil {
return nil, err
}
@ -169,7 +169,7 @@ func deserializeLoopInContract(value []byte) (*LoopInContract, error) {
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = r.Read(contract.ReceiverKey[:])
n, err = r.Read(contract.HtlcKeys.ReceiverScriptKey[:])
if err != nil {
return nil, err
}

@ -134,7 +134,7 @@ func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
return nil, err
}
n, err := r.Read(contract.SenderKey[:])
n, err := r.Read(contract.HtlcKeys.SenderScriptKey[:])
if err != nil {
return nil, err
}
@ -142,7 +142,7 @@ func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = r.Read(contract.ReceiverKey[:])
n, err = r.Read(contract.HtlcKeys.ReceiverScriptKey[:])
if err != nil {
return nil, err
}
@ -229,7 +229,7 @@ func serializeLoopOutContract(swap *LoopOutContract) (
return nil, err
}
n, err := b.Write(swap.SenderKey[:])
n, err := b.Write(swap.HtlcKeys.SenderScriptKey[:])
if err != nil {
return nil, err
}
@ -237,7 +237,7 @@ func serializeLoopOutContract(swap *LoopOutContract) (
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = b.Write(swap.ReceiverKey[:])
n, err = b.Write(swap.HtlcKeys.ReceiverScriptKey[:])
if err != nil {
return nil, err
}

@ -55,6 +55,9 @@ const (
// HTLC v3 (P2TR) script for swaps.
ProtocolVersionHtlcV3 = 10
// ProtocolVersionMuSig2 will enable MuSig2 signature scheme for loops.
ProtocolVersionMuSig2 ProtocolVersion = 11
// ProtocolVersionUnrecorded is set for swaps were created before we
// started saving protocol version with swaps.
ProtocolVersionUnrecorded ProtocolVersion = math.MaxUint32
@ -65,7 +68,7 @@ const (
// experimentalRPCProtocolVersion defines the RPC protocol version that
// includes all currently experimentally released features.
experimentalRPCProtocolVersion = looprpc.ProtocolVersion_HTLC_V3
experimentalRPCProtocolVersion = looprpc.ProtocolVersion_MUSIG2
)
var (
@ -141,6 +144,9 @@ func (p ProtocolVersion) String() string {
case ProtocolVersionHtlcV3:
return "HTLC V3"
case ProtocolVersionMuSig2:
return "MuSig2"
default:
return "Unknown"
}

@ -112,9 +112,30 @@ var (
// value: concatenation of uint32 values [family, index].
keyLocatorKey = []byte("keylocator")
// senderInternalPubKeyKey is the key that stores the sender's internal
// public key which used when constructing the swap HTLC.
//
// path: loopInBucket/loopOutBucket -> swapBucket[hash]
// -> senderInternalPubKeyKey
// value: serialized public key.
senderInternalPubKeyKey = []byte("sender-internal-pubkey")
// receiverInternalPubKeyKey is the key that stores the receiver's
// internal public key which is used when constructing the swap HTLC.
//
// path: loopInBucket/loopOutBucket -> swapBucket[hash]
// -> receiverInternalPubKeyKey
// value: serialized public key.
receiverInternalPubKeyKey = []byte("receiver-internal-pubkey")
byteOrder = binary.BigEndian
// keyLength is the length of a serialized public key.
keyLength = 33
// errInvalidKey is returned when a serialized key is not the expected
// length.
errInvalidKey = fmt.Errorf("invalid serialized key")
)
const (
@ -233,6 +254,95 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) (
}, nil
}
// marshalHtlcKeys marshals the HTLC keys of the swap contract into the swap
// bucket.
func marshalHtlcKeys(swapBucket *bbolt.Bucket, contract *SwapContract) error {
var err error
// Store the key locator for swaps that use taproot HTLCs.
if contract.ProtocolVersion >= ProtocolVersionHtlcV3 {
keyLocator, err := MarshalKeyLocator(
contract.HtlcKeys.ClientScriptKeyLocator,
)
if err != nil {
return err
}
err = swapBucket.Put(keyLocatorKey, keyLocator)
if err != nil {
return err
}
}
// Store the internal keys for MuSig2 swaps.
if contract.ProtocolVersion >= ProtocolVersionMuSig2 {
// Internal pubkeys are always filled.
err = swapBucket.Put(
senderInternalPubKeyKey,
contract.HtlcKeys.SenderInternalPubKey[:],
)
if err != nil {
return err
}
err = swapBucket.Put(
receiverInternalPubKeyKey,
contract.HtlcKeys.ReceiverInternalPubKey[:],
)
if err != nil {
return err
}
}
return nil
}
// unmarshalHtlcKeys deserializes the htlc keys from the swap bucket.
func unmarshalHtlcKeys(swapBucket *bbolt.Bucket, contract *SwapContract) error {
var err error
// HTLC V3 contracts have the client script key locator stored.
if contract.ProtocolVersion >= ProtocolVersionHtlcV3 &&
ProtocolVersionUnrecorded > contract.ProtocolVersion {
contract.HtlcKeys.ClientScriptKeyLocator, err =
UnmarshalKeyLocator(
swapBucket.Get(keyLocatorKey),
)
if err != nil {
return err
}
// Default the internal scriptkeys to the sender and receiver
// keys.
contract.HtlcKeys.SenderInternalPubKey =
contract.HtlcKeys.SenderScriptKey
contract.HtlcKeys.ReceiverInternalPubKey =
contract.HtlcKeys.ReceiverScriptKey
}
// MuSig2 contracts have the internal keys stored too.
if contract.ProtocolVersion >= ProtocolVersionMuSig2 &&
ProtocolVersionUnrecorded > contract.ProtocolVersion {
// The pubkeys used for the joint HTLC internal key are always
// present.
key := swapBucket.Get(senderInternalPubKeyKey)
if len(key) != keyLength {
return errInvalidKey
}
copy(contract.HtlcKeys.SenderInternalPubKey[:], key)
key = swapBucket.Get(receiverInternalPubKeyKey)
if len(key) != keyLength {
return errInvalidKey
}
copy(contract.HtlcKeys.ReceiverInternalPubKey[:], key)
}
return nil
}
// FetchLoopOutSwaps returns all loop out swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
@ -435,19 +545,10 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
return err
}
// Store the key locator for swaps with taproot htlc.
if swap.ProtocolVersion >= ProtocolVersionHtlcV3 {
keyLocator, err := MarshalKeyLocator(
swap.ClientKeyLocator,
)
if err != nil {
return err
}
err = swapBucket.Put(keyLocatorKey, keyLocator)
if err != nil {
return err
}
// Store the htlc keys and server key locator.
err = marshalHtlcKeys(swapBucket, &swap.SwapContract)
if err != nil {
return err
}
// Finally, we'll create an empty updates bucket for this swap
@ -501,19 +602,10 @@ func (s *boltSwapStore) CreateLoopIn(hash lntypes.Hash,
return err
}
// Store the key locator for swaps with taproot htlc.
if swap.ProtocolVersion >= ProtocolVersionHtlcV3 {
keyLocator, err := MarshalKeyLocator(
swap.ClientKeyLocator,
)
if err != nil {
return err
}
err = swapBucket.Put(keyLocatorKey, keyLocator)
if err != nil {
return err
}
// Store the htlc keys and server key locator.
err = marshalHtlcKeys(swapBucket, &swap.SwapContract)
if err != nil {
return err
}
// Finally, we'll create an empty updates bucket for this swap
@ -788,14 +880,12 @@ func (s *boltSwapStore) fetchLoopOutSwap(rootBucket *bbolt.Bucket,
return nil, err
}
// Try to unmarshal the key locator.
if contract.ProtocolVersion >= ProtocolVersionHtlcV3 {
contract.ClientKeyLocator, err = UnmarshalKeyLocator(
swapBucket.Get(keyLocatorKey),
)
if err != nil {
return nil, err
}
// Unmarshal HTLC keys if the contract is recent.
err = unmarshalHtlcKeys(
swapBucket, &contract.SwapContract,
)
if err != nil {
return nil, err
}
loop := LoopOut{
@ -865,14 +955,12 @@ func (s *boltSwapStore) fetchLoopInSwap(rootBucket *bbolt.Bucket,
return nil, err
}
// Try to unmarshal the key locator.
if contract.ProtocolVersion >= ProtocolVersionHtlcV3 {
contract.ClientKeyLocator, err = UnmarshalKeyLocator(
swapBucket.Get(keyLocatorKey),
)
if err != nil {
return nil, err
}
// Unmarshal HTLC keys if the contract is recent.
err = unmarshalHtlcKeys(
swapBucket, &contract.SwapContract,
)
if err != nil {
return nil, err
}
loop := LoopIn{

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/coreos/bbolt"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
@ -28,6 +29,16 @@ var (
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
}
senderInternalKey = [33]byte{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
}
receiverInternalKey = [33]byte{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
}
testPreimage = lntypes.Preimage([32]byte{
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
@ -51,9 +62,16 @@ func TestLoopOutStore(t *testing.T) {
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
ReceiverKey: receiverKey,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
@ -61,7 +79,8 @@ func TestLoopOutStore(t *testing.T) {
// Convert to/from unix to remove timezone, so that it
// doesn't interfere with DeepEqual.
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
@ -195,18 +214,27 @@ func TestLoopInStore(t *testing.T) {
pendingSwap := LoopInContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
ReceiverKey: receiverKey,
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
// Convert to/from unix to remove timezone, so that it
// doesn't interfere with DeepEqual.
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
ProtocolVersion: ProtocolVersionMuSig2,
},
HtlcConfTarget: 2,
LastHop: &lastHop,

@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
@ -19,6 +20,7 @@ import (
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/chainntnfs"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
@ -189,6 +191,23 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
return nil, err
}
// Default the HTLC internal key to our sender key.
senderInternalPubKey := senderKey
// If this is a MuSig2 swap then we'll generate a brand new key pair
// and will use that as the internal key for the HTLC.
if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
secret, err := sharedSecretFromHash(
globalCtx, cfg.lnd.Signer, swapHash,
)
if err != nil {
return nil, err
}
_, pubKey := btcec.PrivKeyFromBytes(secret[:])
copy(senderInternalPubKey[:], pubKey.SerializeCompressed())
}
// Create a cancellable context that is used for monitoring the probe.
probeWaitCtx, probeWaitCancel := context.WithCancel(globalCtx)
@ -204,8 +223,8 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
// htlc.
log.Infof("Initiating swap request at height %v", currentHeight)
swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
request.Amount, senderKey, swapInvoice, probeInvoice,
request.LastHop, request.Initiator,
request.Amount, senderKey, senderInternalPubKey, swapInvoice,
probeInvoice, request.LastHop, request.Initiator,
)
probeWaitCancel()
if err != nil {
@ -237,19 +256,30 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
ReceiverKey: swapResp.receiverKey,
SenderKey: senderKey,
ClientKeyLocator: keyDesc.KeyLocator,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
Label: request.Label,
ProtocolVersion: loopdb.CurrentProtocolVersion(),
HtlcKeys: loopdb.HtlcKeys{
SenderScriptKey: senderKey,
SenderInternalPubKey: senderKey,
ReceiverScriptKey: swapResp.receiverKey,
ReceiverInternalPubKey: swapResp.receiverKey,
ClientScriptKeyLocator: keyDesc.KeyLocator,
},
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
Label: request.Label,
ProtocolVersion: loopdb.CurrentProtocolVersion(),
},
}
// For MuSig2 swaps we store the proper internal keys that we generated
// and received from the server.
if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
contract.HtlcKeys.SenderInternalPubKey = senderInternalPubKey
contract.HtlcKeys.ReceiverInternalPubKey = swapResp.receiverInternalKey
}
swapKit := newSwapKit(
swapHash, swap.TypeIn,
cfg, &contract.SwapContract,
@ -809,6 +839,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
htlcSpend := false
invoiceFinalized := false
htlcKeyRevealed := false
for !htlcSpend || !invoiceFinalized {
select {
// Spend notification error.
@ -825,6 +856,10 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
return err
}
if invoiceFinalized && !htlcKeyRevealed {
htlcKeyRevealed = s.tryPushHtlcKey(ctx)
}
// The htlc spend is confirmed. Inspect the spending tx to
// determine the final swap state.
case spendDetails := <-spendChan:
@ -888,6 +923,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
}
invoiceFinalized = true
htlcKeyRevealed = s.tryPushHtlcKey(ctx)
// Canceled invoice has no effect on server cost
// balance.
@ -903,6 +939,36 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
return nil
}
// tryPushHtlcKey attempts to push the htlc key to the server. If the server
// returns an error of any kind we'll log it as a warning but won't act as
// the swap execution can just go on without the server gaining knowledge of
// our internal key.
func (s *loopInSwap) tryPushHtlcKey(ctx context.Context) bool {
if s.ProtocolVersion < loopdb.ProtocolVersionMuSig2 {
return false
}
log.Infof("Attempting to reveal internal HTLC key to the server")
internalPrivKey, err := sharedSecretFromHash(
ctx, s.swapConfig.lnd.Signer, s.hash,
)
if err != nil {
s.log.Warnf("Unable to derive HTLC internal private key: %v",
err)
return false
}
err = s.server.PushKey(ctx, s.ProtocolVersion, s.hash, internalPrivKey)
if err != nil {
s.log.Warnf("Internal HTLC key reveal failed: %v", err)
return false
}
return true
}
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
spend *chainntnfs.SpendDetail, htlcValue,
sweepFee btcutil.Amount) error {
@ -975,8 +1041,9 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
sequence := uint32(0)
timeoutTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, sequence, s.htlc, *htlcOutpoint, s.SenderKey,
redeemScript, witnessFunc, htlcValue, fee, s.timeoutAddr,
ctx, s.height, sequence, s.htlc, *htlcOutpoint,
s.HtlcKeys.SenderScriptKey, redeemScript, witnessFunc,
htlcValue, fee, s.timeoutAddr,
)
if err != nil {
return 0, err
@ -1026,3 +1093,18 @@ func (s *loopInSwap) setState(state loopdb.SwapState) {
s.lastUpdateTime = time.Now()
s.state = state
}
// sharedSecretFromHash derives the shared secret from the swap hash using the
// swap.KeyFamily family and zero as index.
func sharedSecretFromHash(ctx context.Context, signer lndclient.SignerClient,
hash lntypes.Hash) ([32]byte, error) {
_, hashPubKey := btcec.PrivKeyFromBytes(hash[:])
return signer.DeriveSharedKey(
ctx, hashPubKey, &keychain.KeyLocator{
Family: keychain.KeyFamily(swap.KeyFamily),
Index: 0,
},
)
}

@ -8,10 +8,8 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/input"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
@ -343,6 +341,7 @@ func TestLoopInResume(t *testing.T) {
loopdb.ProtocolVersionUnrecorded,
loopdb.ProtocolVersionHtlcV2,
loopdb.ProtocolVersionHtlcV3,
loopdb.ProtocolVersionMuSig2,
}
testCases := []struct {
@ -414,8 +413,12 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
Preimage: testPreimage,
AmountRequested: 100000,
CltvExpiry: 744,
ReceiverKey: receiverKey,
SenderKey: senderKey,
HtlcKeys: loopdb.HtlcKeys{
SenderScriptKey: senderKey,
SenderInternalPubKey: senderKey,
ReceiverScriptKey: receiverKey,
ReceiverInternalPubKey: receiverKey,
},
MaxSwapFee: 60000,
MaxMinerFee: 50000,
ProtocolVersion: storedVersion,
@ -445,32 +448,10 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
pendSwap.Loop.Events[0].Cost = cost
}
var (
htlc *swap.Htlc
err error
htlc, err := GetHtlc(
testPreimage.Hash(), &contract.SwapContract,
cfg.lnd.ChainParams,
)
switch GetHtlcScriptVersion(storedVersion) {
case swap.HtlcV2:
htlc, err = swap.NewHtlcV2(
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, testPreimage.Hash(),
cfg.lnd.ChainParams,
)
case swap.HtlcV3:
htlc, err = swap.NewHtlcV3(
input.MuSig2Version040,
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, contract.SenderKey,
contract.ReceiverKey, testPreimage.Hash(),
cfg.lnd.ChainParams,
)
default:
t.Fatalf("unknown HTLC script version")
}
require.NoError(t, err)
err = ctx.store.CreateLoopIn(testPreimage.Hash(), contract)

@ -181,16 +181,20 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
SenderKey: swapResp.senderKey,
ReceiverKey: receiverKey,
ClientKeyLocator: keyDesc.KeyLocator,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: request.Expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
Label: request.Label,
ProtocolVersion: loopdb.CurrentProtocolVersion(),
HtlcKeys: loopdb.HtlcKeys{
SenderScriptKey: swapResp.senderKey,
SenderInternalPubKey: swapResp.senderKey,
ReceiverScriptKey: receiverKey,
ReceiverInternalPubKey: receiverKey,
ClientScriptKeyLocator: keyDesc.KeyLocator,
},
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: request.Expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
Label: request.Label,
ProtocolVersion: loopdb.CurrentProtocolVersion(),
},
OutgoingChanSet: chanSet,
}
@ -1351,11 +1355,28 @@ func (s *loopOutSwap) createMuSig2SweepTxn(
return nil, err
}
signers := [][]byte{
s.SenderKey[1:], s.ReceiverKey[1:],
var (
signers [][]byte
muSig2Verion input.MuSig2Version
)
// Depending on the MuSig2 version we either pass 32 byte Schnorr
// public keys or normal 33 byte public keys.
if s.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 {
muSig2Verion = input.MuSig2Version100RC2
signers = [][]byte{
s.HtlcKeys.SenderInternalPubKey[:],
s.HtlcKeys.ReceiverInternalPubKey[:],
}
} else {
muSig2Verion = input.MuSig2Version040
signers = [][]byte{
s.HtlcKeys.SenderInternalPubKey[1:],
s.HtlcKeys.ReceiverInternalPubKey[1:],
}
}
htlc, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3)
htlcScript, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3)
if !ok {
return nil, fmt.Errorf("non taproot htlc")
}
@ -1363,8 +1384,8 @@ func (s *loopOutSwap) createMuSig2SweepTxn(
// Now we're creating a local MuSig2 session using the receiver key's
// key locator and the htlc's root hash.
musig2SessionInfo, err := s.lnd.Signer.MuSig2CreateSession(
ctx, input.MuSig2Version040, &s.ClientKeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(htlc.RootHash[:], false),
ctx, muSig2Verion, &s.HtlcKeys.ClientScriptKeyLocator, signers,
lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false),
)
if err != nil {
return nil, err
@ -1428,7 +1449,7 @@ func (s *loopOutSwap) createMuSig2SweepTxn(
// To be sure that we're good, parse and validate that the combined
// signature is indeed valid for the sig hash and the internal pubkey.
err = s.executeConfig.verifySchnorrSig(
htlc.TaprootKey, sigHash, finalSig,
htlcScript.TaprootKey, sigHash, finalSig,
)
if err != nil {
return nil, err
@ -1609,8 +1630,8 @@ func (s *loopOutSwap) sweep(ctx context.Context, htlcOutpoint wire.OutPoint,
// Create sweep tx.
sweepTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, s.htlc.SuccessSequence(), s.htlc,
htlcOutpoint, s.ReceiverKey, redeemScript, witnessFunc,
htlcValue, fee, s.DestAddr,
htlcOutpoint, s.contract.HtlcKeys.ReceiverScriptKey,
redeemScript, witnessFunc, htlcValue, fee, s.DestAddr,
)
if err != nil {
return err

@ -151,17 +151,22 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err
}
func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
amount btcutil.Amount, _ [33]byte, swapInvoice, _ string,
amount btcutil.Amount, _, _ [33]byte, swapInvoice, _ string,
_ *route.Vertex, _ string) (*newLoopInResponse, error) {
_, receiverKey := test.CreateKey(101)
_, receiverInternalKey := test.CreateKey(102)
if amount != s.expectedSwapAmt {
return nil, errors.New("unexpected test swap amount")
}
var receiverKeyArray [33]byte
var receiverKeyArray, receiverInternalKeyArray [33]byte
copy(receiverKeyArray[:], receiverKey.SerializeCompressed())
copy(
receiverInternalKeyArray[:],
receiverInternalKey.SerializeCompressed(),
)
s.swapInvoice = swapInvoice
s.swapHash = swapHash
@ -175,8 +180,9 @@ func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
<-s.lnd.FailInvoiceChannel
resp := &newLoopInResponse{
expiry: s.height + testChargeOnChainCltvDelta,
receiverKey: receiverKeyArray,
expiry: s.height + testChargeOnChainCltvDelta,
receiverKey: receiverKeyArray,
receiverInternalKey: receiverInternalKeyArray,
}
return resp, nil
@ -261,3 +267,9 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion
return nil, nil, nil
}
func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,
_ lntypes.Hash, _ [32]byte) error {
return nil
}

@ -76,18 +76,27 @@ func GetHtlc(hash lntypes.Hash, contract *loopdb.SwapContract,
switch GetHtlcScriptVersion(contract.ProtocolVersion) {
case swap.HtlcV2:
return swap.NewHtlcV2(
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, hash,
contract.CltvExpiry, contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey, hash,
chainParams,
)
case swap.HtlcV3:
// Swaps that implement the new MuSig2 protocol will be expected
// to use the 1.0RC2 MuSig2 key derivation scheme.
muSig2Version := input.MuSig2Version040
if contract.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 {
muSig2Version = input.MuSig2Version100RC2
}
return swap.NewHtlcV3(
input.MuSig2Version040,
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, contract.SenderKey,
contract.ReceiverKey, hash,
chainParams,
muSig2Version,
contract.CltvExpiry,
contract.HtlcKeys.SenderInternalPubKey,
contract.HtlcKeys.ReceiverInternalPubKey,
contract.HtlcKeys.SenderScriptKey,
contract.HtlcKeys.ReceiverScriptKey,
hash, chainParams,
)
}

@ -94,10 +94,10 @@ type swapServerClient interface {
preimage lntypes.Preimage) error
NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
senderKey [33]byte, swapInvoice, probeInvoice string,
lastHop *route.Vertex, initiator string) (*newLoopInResponse,
error)
swapHash lntypes.Hash, amount btcutil.Amount, senderScriptKey,
senderInternalKey [33]byte, swapInvoice, probeInvoice string,
lastHop *route.Vertex, initiator string) (
*newLoopInResponse, error)
// SubscribeLoopOutUpdates subscribes to loop out server state.
SubscribeLoopOutUpdates(ctx context.Context,
@ -128,6 +128,12 @@ type swapServerClient interface {
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte) (
[]byte, []byte, error)
// PushKey sends the client's HTLC internal key associated with the
// swap to the server.
PushKey(ctx context.Context,
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
clientInternalPrivateKey [32]byte) error
}
type grpcSwapServerClient struct {
@ -419,9 +425,9 @@ func (s *grpcSwapServerClient) PushLoopOutPreimage(ctx context.Context,
}
func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte,
swapInvoice, probeInvoice string, lastHop *route.Vertex,
initiator string) (*newLoopInResponse, error) {
swapHash lntypes.Hash, amount btcutil.Amount, senderScriptKey,
senderInternalKey [33]byte, swapInvoice, probeInvoice string,
lastHop *route.Vertex, initiator string) (*newLoopInResponse, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
@ -429,7 +435,7 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
req := &looprpc.ServerLoopInRequest{
SwapHash: swapHash[:],
Amt: uint64(amount),
SenderKey: senderKey[:],
SenderKey: senderScriptKey[:],
SwapInvoice: swapInvoice,
ProtocolVersion: loopdb.CurrentRPCProtocolVersion(),
ProbeInvoice: probeInvoice,
@ -439,13 +445,19 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
req.LastHop = lastHop[:]
}
// Set the client's internal key if this is a MuSig2 swap.
if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
req.SenderInternalPubkey = senderInternalKey[:]
}
swapResp, err := s.server.NewLoopInSwap(rpcCtx, req)
if err != nil {
return nil, err
}
var receiverKey [33]byte
var receiverKey, receiverInternalKey [33]byte
copy(receiverKey[:], swapResp.ReceiverKey)
copy(receiverInternalKey[:], swapResp.ReceiverInternalPubkey)
// Validate receiver key.
_, err = btcec.ParsePubKey(receiverKey[:])
@ -454,9 +466,10 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
}
return &newLoopInResponse{
receiverKey: receiverKey,
expiry: swapResp.Expiry,
serverMessage: swapResp.ServerMessage,
receiverKey: receiverKey,
receiverInternalKey: receiverInternalKey,
expiry: swapResp.Expiry,
serverMessage: swapResp.ServerMessage,
}, nil
}
@ -753,6 +766,25 @@ func (s *grpcSwapServerClient) MuSig2SignSweep(ctx context.Context,
return res.Nonce, res.PartialSignature, nil
}
// PushKey sends the client's HTLC internal key associated with the swap to
// the server.
func (s *grpcSwapServerClient) PushKey(ctx context.Context,
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
clientInternalPrivateKey [32]byte) error {
req := &looprpc.ServerPushKeyReq{
ProtocolVersion: looprpc.ProtocolVersion(protocolVersion),
SwapHash: swapHash[:],
InternalPrivkey: clientInternalPrivateKey[:],
}
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
_, err := s.server.PushKey(rpcCtx, req)
return err
}
func rpcRouteCancel(details *outCancelDetails) (
*looprpc.CancelLoopOutSwapRequest_RouteCancel, error) {
@ -873,7 +905,8 @@ type newLoopOutResponse struct {
}
type newLoopInResponse struct {
receiverKey [33]byte
expiry int32
serverMessage string
receiverKey [33]byte
receiverInternalKey [33]byte
expiry int32
serverMessage string
}

File diff suppressed because it is too large Load Diff

@ -45,6 +45,8 @@ service SwapServer {
returns (ReportRoutingResultRes);
rpc MuSig2SignSweep (MuSig2SignSweepReq) returns (MuSig2SignSweepRes);
rpc PushKey (ServerPushKeyReq) returns (ServerPushKeyRes);
}
/**
@ -97,6 +99,9 @@ enum ProtocolVersion {
// The client will use the new v3 (taproot) HTLC scripts.
HTLC_V3 = 10;
// Enables MuSig2 signature scheme for swaps.
MUSIG2 = 11;
}
message ServerLoopOutRequest {
@ -196,14 +201,21 @@ message ServerLoopOutTerms {
message ServerLoopInRequest {
bytes sender_key = 1;
bytes sender_internal_pubkey = 9;
bytes swap_hash = 2;
uint64 amt = 3;
string swap_invoice = 4;
bytes last_hop = 5;
/// The protocol version that the client adheres to.
ProtocolVersion protocol_version = 6;
// An invoice that can be used for the purpose of probing.
string probe_invoice = 7;
// The user agent string that identifies the software running on the user's
@ -218,6 +230,9 @@ message ServerLoopInRequest {
message ServerLoopInResponse {
bytes receiver_key = 1;
bytes receiver_internal_pubkey = 9;
int32 expiry = 2;
// A human-readable message from the loop server.
@ -458,7 +473,7 @@ message CancelLoopOutSwapResponse {
}
message ServerProbeRequest {
/// The protocol version that the client adheres to.
// The protocol version that the client adheres to.
ProtocolVersion protocol_version = 1;
// The probe amount.
@ -470,9 +485,7 @@ message ServerProbeRequest {
// Optional last hop to use when probing the client.
bytes last_hop = 4;
/*
Optional route hints to reach the destination through private channels.
*/
// Optional route hints to reach the destination through private channels.
repeated RouteHint route_hints = 5;
}
@ -555,3 +568,17 @@ message MuSig2SignSweepRes {
// The partial signature of the server for the requested sighash.
bytes partial_signature = 2;
}
message ServerPushKeyReq {
// The protocol version that the client adheres to.
ProtocolVersion protocol_version = 1;
// The swap hash.
bytes swap_hash = 2;
// The clients private key used in the HTLCs aggregated internal key.
bytes internal_privkey = 3;
}
message ServerPushKeyRes {
}

@ -32,6 +32,7 @@ type SwapServerClient interface {
RecommendRoutingPlugin(ctx context.Context, in *RecommendRoutingPluginReq, opts ...grpc.CallOption) (*RecommendRoutingPluginRes, error)
ReportRoutingResult(ctx context.Context, in *ReportRoutingResultReq, opts ...grpc.CallOption) (*ReportRoutingResultRes, error)
MuSig2SignSweep(ctx context.Context, in *MuSig2SignSweepReq, opts ...grpc.CallOption) (*MuSig2SignSweepRes, error)
PushKey(ctx context.Context, in *ServerPushKeyReq, opts ...grpc.CallOption) (*ServerPushKeyRes, error)
}
type swapServerClient struct {
@ -214,6 +215,15 @@ func (c *swapServerClient) MuSig2SignSweep(ctx context.Context, in *MuSig2SignSw
return out, nil
}
func (c *swapServerClient) PushKey(ctx context.Context, in *ServerPushKeyReq, opts ...grpc.CallOption) (*ServerPushKeyRes, error) {
out := new(ServerPushKeyRes)
err := c.cc.Invoke(ctx, "/looprpc.SwapServer/PushKey", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapServerServer is the server API for SwapServer service.
// All implementations must embed UnimplementedSwapServerServer
// for forward compatibility
@ -232,6 +242,7 @@ type SwapServerServer interface {
RecommendRoutingPlugin(context.Context, *RecommendRoutingPluginReq) (*RecommendRoutingPluginRes, error)
ReportRoutingResult(context.Context, *ReportRoutingResultReq) (*ReportRoutingResultRes, error)
MuSig2SignSweep(context.Context, *MuSig2SignSweepReq) (*MuSig2SignSweepRes, error)
PushKey(context.Context, *ServerPushKeyReq) (*ServerPushKeyRes, error)
mustEmbedUnimplementedSwapServerServer()
}
@ -281,6 +292,9 @@ func (UnimplementedSwapServerServer) ReportRoutingResult(context.Context, *Repor
func (UnimplementedSwapServerServer) MuSig2SignSweep(context.Context, *MuSig2SignSweepReq) (*MuSig2SignSweepRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method MuSig2SignSweep not implemented")
}
func (UnimplementedSwapServerServer) PushKey(context.Context, *ServerPushKeyReq) (*ServerPushKeyRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method PushKey not implemented")
}
func (UnimplementedSwapServerServer) mustEmbedUnimplementedSwapServerServer() {}
// UnsafeSwapServerServer may be embedded to opt out of forward compatibility for this service.
@ -552,6 +566,24 @@ func _SwapServer_MuSig2SignSweep_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _SwapServer_PushKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerPushKeyReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).PushKey(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapServer/PushKey",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).PushKey(ctx, req.(*ServerPushKeyReq))
}
return interceptor(ctx, in, info, handler)
}
// SwapServer_ServiceDesc is the grpc.ServiceDesc for SwapServer service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -607,6 +639,10 @@ var SwapServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "MuSig2SignSweep",
Handler: _SwapServer_MuSig2SignSweep_Handler,
},
{
MethodName: "PushKey",
Handler: _SwapServer_PushKey_Handler,
},
},
Streams: []grpc.StreamDesc{
{

Loading…
Cancel
Save