From 638973dce22955718fd606ae0328c9d1d29bc202 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 29 Mar 2022 11:31:23 +0200 Subject: [PATCH 1/3] swap: add locking conditions to HtlcScript interface Use of the Script() function is problematic when we introduce taproot because our script will vary depending whether we use keyspend or a tapleaf spend path (and on the tapleaf spent). This has not previously been a problem for segwitv0 scripts, because they contain all of the logical branches for each of our spend conditions in a single script. This commit prepares for removal of the Script() function by moving our address/pkScript/sigScript generation (which need Script()) into each script's implementation of the HtlcScript interface so that they have access to the script directly. --- swap/htlc.go | 142 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 46 deletions(-) diff --git a/swap/htlc.go b/swap/htlc.go index 6f5a44d5a..2c20e91fb 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -65,6 +65,11 @@ type HtlcScript interface { // Script returns the htlc script. Script() []byte + // lockingConditions return the address, pkScript and sigScript (if + // required) for a htlc script. + lockingConditions(HtlcOutputType, *chaincfg.Params) (btcutil.Address, + []byte, []byte, error) + // MaxSuccessWitnessSize returns the maximum witness size for the // success case witness. MaxSuccessWitnessSize() int @@ -190,14 +195,36 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, return nil, err } - var pkScript, sigScript []byte - var address btcutil.Address + address, pkScript, sigScript, err := htlc.lockingConditions( + outputType, chainParams, + ) + if err != nil { + return nil, fmt.Errorf("could not get address: %w", err) + } + + return &Htlc{ + HtlcScript: htlc, + Hash: hash, + Version: version, + PkScript: pkScript, + OutputType: outputType, + ChainParams: chainParams, + Address: address, + SigScript: sigScript, + }, nil +} + +// segwitV0LockingConditions provides the address, pkScript and sigScript (if +// required) for the segwit v0 script and output type provided. +func segwitV0LockingConditions(outputType HtlcOutputType, + chainParams *chaincfg.Params, script []byte) (btcutil.Address, + []byte, []byte, error) { switch outputType { case HtlcNP2WSH: - p2wshPkScript, err := input.WitnessScriptHash(htlc.Script()) + p2wshPkScript, err := input.WitnessScriptHash(script) if err != nil { - return nil, err + return nil, nil, nil, err } // Generate p2sh script for p2wsh (nested). @@ -210,78 +237,54 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, builder.AddData(hash160) builder.AddOp(txscript.OP_EQUAL) - pkScript, err = builder.Script() + pkScript, err := builder.Script() if err != nil { - return nil, err + return nil, nil, nil, err } // Generate a valid sigScript that will allow us to spend the // p2sh output. The sigScript will contain only a single push of // the p2wsh witness program corresponding to the matching // public key of this address. - sigScript, err = txscript.NewScriptBuilder(). + sigScript, err := txscript.NewScriptBuilder(). AddData(p2wshPkScript). Script() if err != nil { - return nil, err + return nil, nil, nil, err } - address, err = btcutil.NewAddressScriptHash( + address, err := btcutil.NewAddressScriptHash( p2wshPkScript, chainParams, ) if err != nil { - return nil, err + return nil, nil, nil, err } + return address, pkScript, sigScript, nil + case HtlcP2WSH: - pkScript, err = input.WitnessScriptHash(htlc.Script()) + pkScript, err := input.WitnessScriptHash(script) if err != nil { - return nil, err + return nil, nil, nil, err } - address, err = btcutil.NewAddressWitnessScriptHash( + address, err := btcutil.NewAddressWitnessScriptHash( pkScript[2:], chainParams, ) if err != nil { - return nil, err - } - - case HtlcP2TR: - // Confirm we have a v3 htlc. - trHtlc, ok := htlc.(*HtlcScriptV3) - if !ok { - return nil, ErrInvalidOutputSelected - } - - // Generate a tapscript address from our HTLC's taptree. - address, err = btcutil.NewAddressTaproot( - schnorr.SerializePubKey(trHtlc.TaprootKey), chainParams, - ) - if err != nil { - return nil, err + return nil, nil, nil, err } - // Generate locking script. - pkScript, err = txscript.PayToAddrScript(address) - if err != nil { - return nil, err - } + // Pay to witness script hash (segwit v0) does not need a + // sigScript (we provide it in the witness instead), so we + // return nil for our sigScript. + return address, pkScript, nil, nil default: - return nil, errors.New("unknown output type") + return nil, nil, nil, fmt.Errorf("unexpected output type: %d", + outputType) } - - return &Htlc{ - HtlcScript: htlc, - Hash: hash, - Version: version, - PkScript: pkScript, - OutputType: outputType, - ChainParams: chainParams, - Address: address, - SigScript: sigScript, - }, nil } // GenSuccessWitness returns the success script to spend this htlc with @@ -491,6 +494,14 @@ func (h *HtlcScriptV1) SuccessSequence() uint32 { return 0 } +// lockingConditions return the address, pkScript and sigScript (if +// required) for a htlc script. +func (h *HtlcScriptV1) lockingConditions(htlcOutputType HtlcOutputType, + params *chaincfg.Params) (btcutil.Address, []byte, []byte, error) { + + return segwitV0LockingConditions(htlcOutputType, params, h.script) +} + // HtlcScriptV2 encapsulates the htlc v2 script. type HtlcScriptV2 struct { script []byte @@ -625,6 +636,14 @@ func (h *HtlcScriptV2) SuccessSequence() uint32 { return 1 } +// lockingConditions return the address, pkScript and sigScript (if +// required) for a htlc script. +func (h *HtlcScriptV2) lockingConditions(htlcOutputType HtlcOutputType, + params *chaincfg.Params) (btcutil.Address, []byte, []byte, error) { + + return segwitV0LockingConditions(htlcOutputType, params, h.script) +} + // HtlcScriptV3 encapsulates the htlc v3 script. type HtlcScriptV3 struct { // The final locking script for the timeout path which is available to @@ -861,3 +880,34 @@ func (h *HtlcScriptV3) MaxTimeoutWitnessSize() int { func (h *HtlcScriptV3) SuccessSequence() uint32 { return 1 } + +// lockingConditions return the address, pkScript and sigScript (if required) +// for a htlc script. +func (h *HtlcScriptV3) lockingConditions(outputType HtlcOutputType, + chainParams *chaincfg.Params) (btcutil.Address, []byte, []byte, error) { + + // HtlcV3 can only have taproot output type, because we utilize + // tapscript claim paths. + if outputType != HtlcP2TR { + return nil, nil, nil, fmt.Errorf("htlc v3 only supports P2TR "+ + "outputs, got: %v", outputType) + } + + // Generate a tapscript address from our tree. + address, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(h.TaprootKey), chainParams, + ) + if err != nil { + return nil, nil, nil, err + } + + // Generate locking script. + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, nil, nil, err + } + + // Taproot (segwit v1) does not need a sigScript (we provide it in the + // witness instead), so we return nil for our sigScript. + return address, pkScript, nil, nil +} From 9610becebd5495228cf6d60e0f80f1c322da40ff Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Wed, 11 May 2022 16:37:50 +0200 Subject: [PATCH 2/3] multi: add the aggregate internal pubkey to the v3 htlc --- client.go | 6 ++-- client_test.go | 2 +- loopd/view.go | 4 +-- loopin_test.go | 2 +- swap.go | 2 +- swap/htlc.go | 91 +++++++++++++++++++---------------------------- swap/htlc_test.go | 30 +++------------- 7 files changed, 50 insertions(+), 87 deletions(-) diff --git a/client.go b/client.go index 4037cc2cb..b6bed801c 100644 --- a/client.go +++ b/client.go @@ -195,7 +195,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlc, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcP2WSH, + swp.Contract.ReceiverKey, swp.Hash, swap.HtlcP2WSH, s.lndServices.ChainParams, ) if err != nil { @@ -216,7 +216,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlcNP2WSH, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcNP2WSH, + swp.Contract.ReceiverKey, swp.Hash, swap.HtlcNP2WSH, s.lndServices.ChainParams, ) if err != nil { @@ -226,7 +226,7 @@ func (s *Client) FetchSwaps() ([]*SwapInfo, error) { htlcP2WSH, err := swap.NewHtlc( GetHtlcScriptVersion(swp.Contract.ProtocolVersion), swp.Contract.CltvExpiry, swp.Contract.SenderKey, - swp.Contract.ReceiverKey, nil, swp.Hash, swap.HtlcP2WSH, + swp.Contract.ReceiverKey, swp.Hash, swap.HtlcP2WSH, s.lndServices.ChainParams, ) if err != nil { diff --git a/client_test.go b/client_test.go index 426ee6ce4..6a315bcec 100644 --- a/client_test.go +++ b/client_test.go @@ -284,7 +284,7 @@ func testResume(t *testing.T, confs uint32, expired, preimageRevealed, scriptVersion := GetHtlcScriptVersion(protocolVersion) htlc, err := swap.NewHtlc( scriptVersion, pendingSwap.Contract.CltvExpiry, senderKey, - receiverKey, nil, hash, swap.HtlcP2WSH, &chaincfg.TestNet3Params, + receiverKey, hash, swap.HtlcP2WSH, &chaincfg.TestNet3Params, ) require.NoError(t, err) require.Equal(t, htlc.PkScript, confIntent.PkScript) diff --git a/loopd/view.go b/loopd/view.go index a3d40ee3e..c13c8bae4 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -54,7 +54,7 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error { s.Contract.CltvExpiry, s.Contract.SenderKey, s.Contract.ReceiverKey, - nil, s.Hash, swap.HtlcP2WSH, chainParams, + s.Hash, swap.HtlcP2WSH, chainParams, ) if err != nil { return err @@ -106,7 +106,7 @@ func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error { s.Contract.CltvExpiry, s.Contract.SenderKey, s.Contract.ReceiverKey, - nil, s.Hash, swap.HtlcNP2WSH, chainParams, + s.Hash, swap.HtlcNP2WSH, chainParams, ) if err != nil { return err diff --git a/loopin_test.go b/loopin_test.go index 8bfe4019a..bdf47dae2 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -399,7 +399,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, htlc, err := swap.NewHtlc( scriptVersion, contract.CltvExpiry, contract.SenderKey, - contract.ReceiverKey, nil, testPreimage.Hash(), swap.HtlcNP2WSH, + contract.ReceiverKey, testPreimage.Hash(), swap.HtlcNP2WSH, cfg.lnd.ChainParams, ) if err != nil { diff --git a/swap.go b/swap.go index 3f778c2a4..193bddeba 100644 --- a/swap.go +++ b/swap.go @@ -72,7 +72,7 @@ func (s *swapKit) getHtlc(outputType swap.HtlcOutputType) (*swap.Htlc, error) { return swap.NewHtlc( GetHtlcScriptVersion(s.contract.ProtocolVersion), s.contract.CltvExpiry, s.contract.SenderKey, - s.contract.ReceiverKey, nil, s.hash, outputType, + s.contract.ReceiverKey, s.hash, outputType, s.swapConfig.lnd.ChainParams, ) } diff --git a/swap/htlc.go b/swap/htlc.go index 2c20e91fb..5be9e357e 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -6,10 +6,12 @@ import ( "errors" "fmt" - btcec "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -106,7 +108,7 @@ var ( // script size. QuoteHtlc, _ = NewHtlc( HtlcV2, - ^int32(0), quoteKey, quoteKey, nil, quoteHash, HtlcP2WSH, + ^int32(0), quoteKey, quoteKey, quoteHash, HtlcP2WSH, &chaincfg.MainNetParams, ) @@ -119,17 +121,6 @@ var ( // selected for a v1 or v2 script. ErrInvalidOutputSelected = fmt.Errorf("taproot output selected for " + "non taproot htlc") - - // ErrSharedKeyNotNeeded is returned when a shared key is provided for - // either the v1 or v2 script. Shared key is only necessary for the v3 - // script. - ErrSharedKeyNotNeeded = fmt.Errorf("shared key not supported for " + - "script version") - - // ErrSharedKeyRequired is returned when a script version requires a - // shared key. - ErrSharedKeyRequired = fmt.Errorf("shared key required for script " + - "version") ) // String returns the string value of HtlcOutputType. @@ -152,9 +143,8 @@ func (h HtlcOutputType) String() string { // NewHtlc returns a new instance. For v3 scripts, an internal pubkey generated // by both participants must be provided. func NewHtlc(version ScriptVersion, cltvExpiry int32, - senderKey, receiverKey [33]byte, sharedKey *btcec.PublicKey, - hash lntypes.Hash, outputType HtlcOutputType, - chainParams *chaincfg.Params) (*Htlc, error) { + senderKey, receiverKey [33]byte, hash lntypes.Hash, + outputType HtlcOutputType, chainParams *chaincfg.Params) (*Htlc, error) { var ( err error @@ -163,28 +153,18 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, switch version { case HtlcV1: - if sharedKey != nil { - return nil, ErrSharedKeyNotNeeded - } htlc, err = newHTLCScriptV1( cltvExpiry, senderKey, receiverKey, hash, ) case HtlcV2: - if sharedKey != nil { - return nil, ErrSharedKeyNotNeeded - } htlc, err = newHTLCScriptV2( cltvExpiry, senderKey, receiverKey, hash, ) case HtlcV3: - if sharedKey == nil { - return nil, ErrSharedKeyRequired - } htlc, err = newHTLCScriptV3( - cltvExpiry, senderKey, receiverKey, - sharedKey, hash, + cltvExpiry, senderKey, receiverKey, hash, ) default: @@ -646,49 +626,51 @@ func (h *HtlcScriptV2) lockingConditions(htlcOutputType HtlcOutputType, // HtlcScriptV3 encapsulates the htlc v3 script. type HtlcScriptV3 struct { - // The final locking script for the timeout path which is available to - // the sender after the set blockheight. + // TimeoutScript is the final locking script for the timeout path which + // is available to the sender after the set blockheight. TimeoutScript []byte - // The final locking script for the success path in which the receiver - // reveals the preimage. + // SuccessScript is the final locking script for the success path in + // which the receiver reveals the preimage. SuccessScript []byte - // The public key for the keyspend path which bypasses the above two - // locking scripts. + // InternalPubKey is the public key for the keyspend path which bypasses + // the above two locking scripts. InternalPubKey *btcec.PublicKey - // The taproot public key which is created with the above 3 inputs. + // TaprootKey is the taproot public key which is created with the above + // 3 inputs. TaprootKey *btcec.PublicKey + + // RootHash is the root hash of the taptree. + RootHash chainhash.Hash } // newHTLCScriptV3 constructs a HtlcScipt with the HTLC V3 taproot script. -func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, - receiverHtlcKey [33]byte, sharedKey *btcec.PublicKey, +func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, receiverHtlcKey [33]byte, swapHash lntypes.Hash) (*HtlcScriptV3, error) { - receiverPubKey, err := btcec.ParsePubKey( - receiverHtlcKey[:], - ) + senderPubKey, err := schnorr.ParsePubKey(senderHtlcKey[1:]) if err != nil { return nil, err } - senderPubKey, err := btcec.ParsePubKey( - senderHtlcKey[:], - ) + receiverPubKey, err := schnorr.ParsePubKey(receiverHtlcKey[1:]) if err != nil { return nil, err } - var schnorrSenderKey, schnorrReceiverKey [32]byte - copy(schnorrSenderKey[:], schnorr.SerializePubKey(senderPubKey)) - copy(schnorrReceiverKey[:], schnorr.SerializePubKey(receiverPubKey)) + aggregateKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{senderPubKey, receiverPubKey}, true, + ) + if err != nil { + return nil, err + } // Create our success path script, we'll use this separately // to generate the success path leaf. successPathScript, err := GenSuccessPathScript( - schnorrReceiverKey, swapHash, + receiverPubKey, swapHash, ) if err != nil { return nil, err @@ -697,7 +679,7 @@ func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, // Create our timeout path leaf, we'll use this separately // to generate the timeout path leaf. timeoutPathScript, err := GenTimeoutPathScript( - schnorrSenderKey, int64(cltvExpiry), + senderPubKey, int64(cltvExpiry), ) if err != nil { return nil, err @@ -713,14 +695,15 @@ func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, // Calculate top level taproot key. taprootKey := txscript.ComputeTaprootOutputKey( - sharedKey, rootHash[:], + aggregateKey.PreTweakedKey, rootHash[:], ) return &HtlcScriptV3{ TimeoutScript: timeoutPathScript, SuccessScript: successPathScript, - InternalPubKey: sharedKey, + InternalPubKey: aggregateKey.PreTweakedKey, TaprootKey: taprootKey, + RootHash: rootHash, }, nil } @@ -728,11 +711,11 @@ func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, // Largest possible bytesize of the script is 32 + 1 + 2 + 1 = 36. // // OP_CHECKSIGVERIFY OP_CHECKLOCKTIMEVERIFY -func GenTimeoutPathScript( - senderHtlcKey [32]byte, cltvExpiry int64) ([]byte, error) { +func GenTimeoutPathScript(senderHtlcKey *btcec.PublicKey, cltvExpiry int64) ( + []byte, error) { builder := txscript.NewScriptBuilder() - builder.AddData(senderHtlcKey[:]) + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) builder.AddOp(txscript.OP_CHECKSIGVERIFY) builder.AddInt64(cltvExpiry) builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) @@ -746,12 +729,12 @@ func GenTimeoutPathScript( // OP_SIZE 32 OP_EQUALVERIFY // OP_HASH160 OP_EQUALVERIFY // 1 OP_CHECKSEQUENCEVERIFY -func GenSuccessPathScript(receiverHtlcKey [32]byte, +func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey, swapHash lntypes.Hash) ([]byte, error) { builder := txscript.NewScriptBuilder() - builder.AddData(receiverHtlcKey[:]) + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) builder.AddOp(txscript.OP_CHECKSIGVERIFY) builder.AddOp(txscript.OP_SIZE) builder.AddInt64(32) diff --git a/swap/htlc_test.go b/swap/htlc_test.go index 742f67760..0bf9b9e28 100644 --- a/swap/htlc_test.go +++ b/swap/htlc_test.go @@ -3,12 +3,10 @@ package swap import ( "bytes" "crypto/sha256" - "encoding/hex" "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -137,7 +135,7 @@ func TestHtlcV2(t *testing.T) { // Create the htlc. htlc, err := NewHtlc( - HtlcV2, testCltvExpiry, senderKey, receiverKey, nil, hash, + HtlcV2, testCltvExpiry, senderKey, receiverKey, hash, HtlcP2WSH, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -285,7 +283,7 @@ func TestHtlcV2(t *testing.T) { // Create the htlc with the bogus key. htlc, err = NewHtlc( HtlcV2, testCltvExpiry, - bogusKey, receiverKey, nil, hash, + bogusKey, receiverKey, hash, HtlcP2WSH, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -352,17 +350,8 @@ func TestHtlcV3(t *testing.T) { copy(receiverKey[:], receiverPubKey.SerializeCompressed()) copy(senderKey[:], senderPubKey.SerializeCompressed()) - randomSharedKey, err := hex.DecodeString( - "03fcb7d1b502bd59f4dbc6cf503e5c280189e0e6dd2d10c4c14d97ed8611" + - "a99178", - ) - require.NoError(t, err) - - randomSharedPubKey, err := btcec.ParsePubKey(randomSharedKey) - require.NoError(t, err) - htlc, err := NewHtlc( - HtlcV3, cltvExpiry, senderKey, receiverKey, randomSharedPubKey, + HtlcV3, cltvExpiry, senderKey, receiverKey, hashedPreimage, HtlcP2TR, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -544,18 +533,9 @@ func TestHtlcV3(t *testing.T) { bogusKey.SerializeCompressed(), ) - var shnorrSenderKey [32]byte - copy( - shnorrSenderKey[:], - schnorr.SerializePubKey( - senderPubKey, - ), - ) - htlc, err := NewHtlc( HtlcV3, cltvExpiry, bogusKeyBytes, - receiverKey, randomSharedPubKey, - hashedPreimage, HtlcP2TR, + receiverKey, hashedPreimage, HtlcP2TR, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -576,7 +556,7 @@ func TestHtlcV3(t *testing.T) { ) timeoutScript, err := GenTimeoutPathScript( - shnorrSenderKey, int64(cltvExpiry), + senderPubKey, int64(cltvExpiry), ) require.NoError(t, err) From c7ef4297c050c09941e1c6be722e9abcc43bcc18 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Tue, 29 Mar 2022 13:17:19 +0200 Subject: [PATCH 3/3] multi: update sweeping to allow different sighashes and claim scripts Taproot spends require a different sighash, so we update our HtlcScript interface to provide the appropriate sighash when sweeping. We also add distinct Timeout/Success Script functions to allow for tapleaf spends which have different locking scripts for different paths. Note that the timeout and success paths will be the same for segwit v0 htlcs, because it has a single branched script containing all spend paths. In future iterations, this differentiation of claim scripts can also be used to use musig2 to collaboratively keyspend P2TR htlcs with the server. This script can be expressed as PriorityScript (because we'll try to keyspend as a priority, and then fall back to a tap leaf spend). As we've done here, segwit v0 spends would just return their single script for PriorityScript, and the claim would be no different from our other claims. --- loopin.go | 6 ++- loopout.go | 6 ++- swap/htlc.go | 105 +++++++++++++++++++++++++++++++++++----------- swap/htlc_test.go | 25 +++++++---- sweep/sweeper.go | 18 ++++++-- 5 files changed, 121 insertions(+), 39 deletions(-) diff --git a/loopin.go b/loopin.go index 6eb0f4aae..d4e7e5716 100644 --- a/loopin.go +++ b/loopin.go @@ -945,14 +945,18 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context, return 0, err } + // Create a function that will assemble our timeout witness. witnessFunc := func(sig []byte) (wire.TxWitness, error) { return s.htlc.GenTimeoutWitness(sig) } + // Retrieve the full script required to unlock the output. + redeemScript := s.htlc.TimeoutScript() + sequence := uint32(0) timeoutTx, err := s.sweeper.CreateSweepTx( ctx, s.height, sequence, s.htlc, *htlcOutpoint, s.SenderKey, - witnessFunc, htlcValue, fee, s.timeoutAddr, + redeemScript, witnessFunc, htlcValue, fee, s.timeoutAddr, ) if err != nil { return 0, err diff --git a/loopout.go b/loopout.go index 97e8750ae..ecfcac1d7 100644 --- a/loopout.go +++ b/loopout.go @@ -1240,6 +1240,9 @@ func (s *loopOutSwap) sweep(ctx context.Context, return s.htlc.GenSuccessWitness(sig, s.Preimage) } + // Retrieve the full script required to unlock the output. + redeemScript := s.htlc.SuccessScript() + remainingBlocks := s.CltvExpiry - s.height blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta preimageRevealed := s.state == loopdb.StatePreimageRevealed @@ -1296,7 +1299,8 @@ func (s *loopOutSwap) sweep(ctx context.Context, // Create sweep tx. sweepTx, err := s.sweeper.CreateSweepTx( ctx, s.height, s.htlc.SuccessSequence(), s.htlc, htlcOutpoint, - s.ReceiverKey, witnessFunc, htlcValue, fee, s.DestAddr, + s.ReceiverKey, redeemScript, witnessFunc, htlcValue, fee, + s.DestAddr, ) if err != nil { return err diff --git a/swap/htlc.go b/swap/htlc.go index 5be9e357e..56d961895 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -64,9 +64,6 @@ type HtlcScript interface { // redeeming the htlc. IsSuccessWitness(witness wire.TxWitness) bool - // Script returns the htlc script. - Script() []byte - // lockingConditions return the address, pkScript and sigScript (if // required) for a htlc script. lockingConditions(HtlcOutputType, *chaincfg.Params) (btcutil.Address, @@ -80,9 +77,21 @@ type HtlcScript interface { // timeout case witness. MaxTimeoutWitnessSize() int + // TimeoutScript returns the redeem script required to unlock the htlc + // after timeout. + TimeoutScript() []byte + + // SuccessScript returns the redeem script required to unlock the htlc + // using the preimage. + SuccessScript() []byte + // SuccessSequence returns the sequence to spend this htlc in the // success case. SuccessSequence() uint32 + + // SigHash is the signature hash to use for transactions spending from + // the htlc. + SigHash() txscript.SigHashType } // Htlc contains relevant htlc information from the receiver perspective. @@ -290,8 +299,8 @@ func (h *Htlc) AddSuccessToEstimator(estimator *input.TxWeightEstimator) error { if !ok { return ErrInvalidOutputSelected } - successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript) - timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript) + successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript()) + timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript()) timeoutLeafHash := timeoutLeaf.TapHash() tapscript := input.TapscriptPartialReveal( @@ -321,8 +330,8 @@ func (h *Htlc) AddTimeoutToEstimator(estimator *input.TxWeightEstimator) error { if !ok { return ErrInvalidOutputSelected } - successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript) - timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript) + successLeaf := txscript.NewBaseTapLeaf(trHtlc.SuccessScript()) + timeoutLeaf := txscript.NewBaseTapLeaf(trHtlc.TimeoutScript()) successLeafHash := successLeaf.TapHash() tapscript := input.TapscriptPartialReveal( @@ -436,8 +445,19 @@ func (h *HtlcScriptV1) IsSuccessWitness(witness wire.TxWitness) bool { return !isTimeoutTx } -// Script returns the htlc script. -func (h *HtlcScriptV1) Script() []byte { +// TimeoutScript returns the redeem script required to unlock the htlc after +// timeout. +// +// In the case of HtlcScriptV1, this is the full segwit v0 script. +func (h *HtlcScriptV1) TimeoutScript() []byte { + return h.script +} + +// SuccessScript returns the redeem script required to unlock the htlc using +// the preimage. +// +// In the case of HtlcScriptV1, this is the full segwit v0 script. +func (h *HtlcScriptV1) SuccessScript() []byte { return h.script } @@ -474,6 +494,11 @@ func (h *HtlcScriptV1) SuccessSequence() uint32 { return 0 } +// Sighash is the signature hash to use for transactions spending from the htlc. +func (h *HtlcScriptV1) SigHash() txscript.SigHashType { + return txscript.SigHashAll +} + // lockingConditions return the address, pkScript and sigScript (if // required) for a htlc script. func (h *HtlcScriptV1) lockingConditions(htlcOutputType HtlcOutputType, @@ -577,8 +602,19 @@ func (h *HtlcScriptV2) GenTimeoutWitness( return witnessStack, nil } -// Script returns the htlc script. -func (h *HtlcScriptV2) Script() []byte { +// TimeoutScript returns the redeem script required to unlock the htlc after +// timeout. +// +// In the case of HtlcScriptV2, this is the full segwit v0 script. +func (h *HtlcScriptV2) TimeoutScript() []byte { + return h.script +} + +// SuccessScript returns the redeem script required to unlock the htlc using +// the preimage. +// +// In the case of HtlcScriptV2, this is the full segwit v0 script. +func (h *HtlcScriptV2) SuccessScript() []byte { return h.script } @@ -616,6 +652,11 @@ func (h *HtlcScriptV2) SuccessSequence() uint32 { return 1 } +// Sighash is the signature hash to use for transactions spending from the htlc. +func (h *HtlcScriptV2) SigHash() txscript.SigHashType { + return txscript.SigHashAll +} + // lockingConditions return the address, pkScript and sigScript (if // required) for a htlc script. func (h *HtlcScriptV2) lockingConditions(htlcOutputType HtlcOutputType, @@ -626,13 +667,13 @@ func (h *HtlcScriptV2) lockingConditions(htlcOutputType HtlcOutputType, // HtlcScriptV3 encapsulates the htlc v3 script. type HtlcScriptV3 struct { - // TimeoutScript is the final locking script for the timeout path which + // timeoutScript is the final locking script for the timeout path which // is available to the sender after the set blockheight. - TimeoutScript []byte + timeoutScript []byte - // SuccessScript is the final locking script for the success path in + // successScript is the final locking script for the success path in // which the receiver reveals the preimage. - SuccessScript []byte + successScript []byte // InternalPubKey is the public key for the keyspend path which bypasses // the above two locking scripts. @@ -699,8 +740,8 @@ func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, receiverHtlcKey [33]byte, ) return &HtlcScriptV3{ - TimeoutScript: timeoutPathScript, - SuccessScript: successPathScript, + timeoutScript: timeoutPathScript, + successScript: successPathScript, InternalPubKey: aggregateKey.PreTweakedKey, TaprootKey: taprootKey, RootHash: rootHash, @@ -779,7 +820,7 @@ func (h *HtlcScriptV3) genControlBlock(leafScript []byte) ([]byte, error) { func (h *HtlcScriptV3) genSuccessWitness( receiverSig []byte, preimage lntypes.Preimage) (wire.TxWitness, error) { - controlBlockBytes, err := h.genControlBlock(h.TimeoutScript) + controlBlockBytes, err := h.genControlBlock(h.timeoutScript) if err != nil { return nil, err } @@ -787,7 +828,7 @@ func (h *HtlcScriptV3) genSuccessWitness( return wire.TxWitness{ preimage[:], receiverSig, - h.SuccessScript, + h.successScript, controlBlockBytes, }, nil } @@ -797,14 +838,14 @@ func (h *HtlcScriptV3) genSuccessWitness( func (h *HtlcScriptV3) GenTimeoutWitness( senderSig []byte) (wire.TxWitness, error) { - controlBlockBytes, err := h.genControlBlock(h.SuccessScript) + controlBlockBytes, err := h.genControlBlock(h.successScript) if err != nil { return nil, err } return wire.TxWitness{ senderSig, - h.TimeoutScript, + h.timeoutScript, controlBlockBytes, }, nil } @@ -815,9 +856,20 @@ func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { return len(witness) == 4 } -// Script is not implemented, but necessary to conform to interface. -func (h *HtlcScriptV3) Script() []byte { - return nil +// TimeoutScript returns the redeem script required to unlock the htlc after +// timeout. +// +// In the case of HtlcScriptV3, this is the timeout tapleaf. +func (h *HtlcScriptV3) TimeoutScript() []byte { + return h.timeoutScript +} + +// SuccessScript returns the redeem script required to unlock the htlc using +// the preimage. +// +// In the case of HtlcScriptV3, this is the claim tapleaf. +func (h *HtlcScriptV3) SuccessScript() []byte { + return h.successScript } // MaxSuccessWitnessSize returns the maximum witness size for the @@ -864,6 +916,11 @@ func (h *HtlcScriptV3) SuccessSequence() uint32 { return 1 } +// Sighash is the signature hash to use for transactions spending from the htlc. +func (h *HtlcScriptV3) SigHash() txscript.SigHashType { + return txscript.SigHashDefault +} + // lockingConditions return the address, pkScript and sigScript (if required) // for a htlc script. func (h *HtlcScriptV3) lockingConditions(outputType HtlcOutputType, diff --git a/swap/htlc_test.go b/swap/htlc_test.go index 0bf9b9e28..96b2f166c 100644 --- a/swap/htlc_test.go +++ b/swap/htlc_test.go @@ -158,16 +158,17 @@ func TestHtlcV2(t *testing.T) { ) signTx := func(tx *wire.MsgTx, pubkey *btcec.PublicKey, - signer *input.MockSigner) (input.Signature, error) { + signer *input.MockSigner, witnessScript []byte) ( + input.Signature, error) { signDesc := &input.SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: pubkey, }, - WitnessScript: htlc.Script(), + WitnessScript: witnessScript, Output: htlcOutput, - HashType: txscript.SigHashAll, + HashType: htlc.SigHash(), SigHashes: txscript.NewTxSigHashes( tx, prevOutFetcher, ), @@ -189,6 +190,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.TxIn[0].Sequence = htlc.SuccessSequence() sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, + htlc.SuccessScript(), ) require.NoError(t, err) @@ -208,6 +210,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.TxIn[0].Sequence = 0 sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, + htlc.SuccessScript(), ) require.NoError(t, err) @@ -226,6 +229,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.LockTime = testCltvExpiry - 1 sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, + htlc.TimeoutScript(), ) require.NoError(t, err) @@ -244,6 +248,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, + htlc.TimeoutScript(), ) require.NoError(t, err) @@ -262,6 +267,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, + htlc.TimeoutScript(), ) require.NoError(t, err) @@ -297,6 +303,7 @@ func TestHtlcV2(t *testing.T) { sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, + htlc.TimeoutScript(), ) require.NoError(t, err) @@ -390,7 +397,7 @@ func TestHtlcV3(t *testing.T) { sig, err := txscript.RawTxInTapscriptSignature( tx, hashCache, 0, value, p2trPkScript, leaf, - txscript.SigHashDefault, privateKey, + htlc.SigHash(), privateKey, ) require.NoError(t, err) @@ -415,7 +422,7 @@ func TestHtlcV3(t *testing.T) { sig := signTx( tx, receiverPrivKey, txscript.NewBaseTapLeaf( - trHtlc.SuccessScript, + trHtlc.SuccessScript(), ), ) witness, err := htlc.genSuccessWitness( @@ -439,7 +446,7 @@ func TestHtlcV3(t *testing.T) { sig := signTx( tx, receiverPrivKey, txscript.NewBaseTapLeaf( - trHtlc.SuccessScript, + trHtlc.SuccessScript(), ), ) witness, err := htlc.genSuccessWitness( @@ -463,7 +470,7 @@ func TestHtlcV3(t *testing.T) { sig := signTx( tx, senderPrivKey, txscript.NewBaseTapLeaf( - trHtlc.TimeoutScript, + trHtlc.TimeoutScript(), ), ) @@ -486,7 +493,7 @@ func TestHtlcV3(t *testing.T) { sig := signTx( tx, senderPrivKey, txscript.NewBaseTapLeaf( - trHtlc.TimeoutScript, + trHtlc.TimeoutScript(), ), ) @@ -509,7 +516,7 @@ func TestHtlcV3(t *testing.T) { sig := signTx( tx, receiverPrivKey, txscript.NewBaseTapLeaf( - trHtlc.TimeoutScript, + trHtlc.TimeoutScript(), ), ) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 223e104b7..3e06e9b64 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -23,7 +23,7 @@ type Sweeper struct { func (s *Sweeper) CreateSweepTx( globalCtx context.Context, height int32, sequence uint32, htlc *swap.Htlc, htlcOutpoint wire.OutPoint, - keyBytes [33]byte, + keyBytes [33]byte, witnessScript []byte, witnessFunc func(sig []byte) (wire.TxWitness, error), amount, fee btcutil.Amount, destAddr btcutil.Address) (*wire.MsgTx, error) { @@ -59,20 +59,30 @@ func (s *Sweeper) CreateSweepTx( } signDesc := lndclient.SignDescriptor{ - WitnessScript: htlc.Script(), + WitnessScript: witnessScript, Output: &wire.TxOut{ Value: int64(amount), PkScript: htlc.PkScript, }, - HashType: txscript.SigHashAll, + HashType: htlc.SigHash(), InputIndex: 0, KeyDesc: keychain.KeyDescriptor{ PubKey: key, }, } + // We need our previous outputs for taproot spends, and there's no + // harm including them for segwit v0, so we always include our prevOut. + prevOut := []*wire.TxOut{ + { + Value: int64(amount), + PkScript: htlc.PkScript, + }, + } + rawSigs, err := s.Lnd.Signer.SignOutputRaw( - globalCtx, sweepTx, []*lndclient.SignDescriptor{&signDesc}, nil, + globalCtx, sweepTx, []*lndclient.SignDescriptor{&signDesc}, + prevOut, ) if err != nil { return nil, fmt.Errorf("signing: %v", err)