From b437f0d9c49df1a02b65dde5195a6bce3f4e3db1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 15 Mar 2024 11:43:21 -0400 Subject: [PATCH] input+lnwallet: update taproot scripts to accept optional aux leaf In this commit, we update all the taproot scripts to also accept an optional aux leaf. This aux leaf can be used to add more redemption paths for advanced channels, or just as an extra commitment space. --- input/script_utils.go | 103 +++++++++++--- input/size_test.go | 21 ++- input/taproot_test.go | 202 +++++++++++++++++++++------- lnwallet/channel.go | 3 + lnwallet/commitment.go | 9 +- watchtower/blob/justice_kit.go | 9 +- watchtower/blob/justice_kit_test.go | 7 +- 7 files changed, 273 insertions(+), 81 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index fde0c6f5e99..aa74266bdc7 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnutils" "golang.org/x/crypto/ripemd160" ) @@ -638,6 +639,12 @@ type HtlcScriptTree struct { // TimeoutTapLeaf is the tapleaf for the timeout path. TimeoutTapLeaf txscript.TapLeaf + // AuxLeaf is an axuillaruy leaf that can be used to extend the base + // HTLC script tree with new spend paths, or just as extra commitment + // space. When present, this leaf will always be in the left-most or + // right-most area of the tapscript tree. + AuxLeaf fn.Option[txscript.TapLeaf] + htlcType htlcType } @@ -714,7 +721,8 @@ var _ TapscriptDescriptor = (*HtlcScriptTree)(nil) // the HTLC key for HTLCs on the sender's commitment. func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - hType htlcType) (*HtlcScriptTree, error) { + hType htlcType, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -731,10 +739,15 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, return nil, err } + tapLeaves := []txscript.TapLeaf{successTapLeaf, timeoutTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With the two leaves obtained, we'll now make the tapscript tree, // then obtain the root from that tapscriptTree := txscript.AssembleTaprootScriptTree( - successTapLeaf, timeoutTapLeaf, + tapLeaves..., ) tapScriptRoot := tapscriptTree.RootNode.TapHash() @@ -754,6 +767,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, }, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, + AuxLeaf: auxLeaf, htlcType: hType, }, nil } @@ -788,7 +802,8 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // unilaterally spend the created output. func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - localCommit bool) (*HtlcScriptTree, error) { + localCommit bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { var hType htlcType if localCommit { @@ -802,7 +817,7 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, // tap leaf paths. return senderHtlcTapScriptTree( senderHtlcKey, receiverHtlcKey, revokeKey, payHash, - hType, + hType, auxLeaf, ) } @@ -1273,7 +1288,8 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // the HTLC key for HTLCs on the receiver's commitment. func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - cltvExpiry uint32, hType htlcType) (*HtlcScriptTree, error) { + cltvExpiry uint32, hType htlcType, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -1290,10 +1306,15 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, return nil, err } + tapLeaves := []txscript.TapLeaf{timeoutTapLeaf, successTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With the two leaves obtained, we'll now make the tapscript tree, // then obtain the root from that tapscriptTree := txscript.AssembleTaprootScriptTree( - timeoutTapLeaf, successTapLeaf, + tapLeaves..., ) tapScriptRoot := tapscriptTree.RootNode.TapHash() @@ -1313,6 +1334,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, }, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, + AuxLeaf: auxLeaf, htlcType: hType, }, nil } @@ -1347,7 +1369,8 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // the tap leaf are returned. func ReceiverHTLCScriptTaproot(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, - payHash []byte, ourCommit bool) (*HtlcScriptTree, error) { + payHash []byte, ourCommit bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { var hType htlcType if ourCommit { @@ -1361,7 +1384,7 @@ func ReceiverHTLCScriptTaproot(cltvExpiry uint32, // tap leaf paths. return receiverHtlcTapScriptTree( senderHtlcKey, receiverHtlcKey, revocationKey, payHash, - cltvExpiry, hType, + cltvExpiry, hType, auxLeaf, ) } @@ -1592,7 +1615,8 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey, // SecondLevelHtlcTapscriptTree construct the indexed tapscript tree needed to // generate the taptweak to create the final output and also control block. func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, - csvDelay uint32) (*txscript.IndexedTapScriptTree, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*txscript.IndexedTapScriptTree, error) { // First grab the second level leaf script we need to create the top // level output. @@ -1601,9 +1625,14 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, return nil, err } + tapLeaves := []txscript.TapLeaf{secondLevelTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // Now that we have the sole second level script, we can create the // tapscript tree that commits to both the leaves. - return txscript.AssembleTaprootScriptTree(secondLevelTapLeaf), nil + return txscript.AssembleTaprootScriptTree(tapLeaves...), nil } // TaprootSecondLevelHtlcScript is the uniform script that's used as the output @@ -1623,12 +1652,13 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, // // The keyspend path require knowledge of the top level revocation private key. func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32) (*btcec.PublicKey, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { // First, we'll make the tapscript tree that commits to the redemption // path. tapScriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey, csvDelay, + delayKey, csvDelay, auxLeaf, ) if err != nil { return nil, err @@ -1653,17 +1683,22 @@ type SecondLevelScriptTree struct { // SuccessTapLeaf is the tapleaf for the redemption path. SuccessTapLeaf txscript.TapLeaf + + // AuxLeaf is an optional leaf that can be used to extend the script + // tree. + AuxLeaf fn.Option[txscript.TapLeaf] } // TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the // second level HTLC output. func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32) (*SecondLevelScriptTree, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*SecondLevelScriptTree, error) { // First, we'll make the tapscript tree that commits to the redemption // path. tapScriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey, csvDelay, + delayKey, csvDelay, auxLeaf, ) if err != nil { return nil, err @@ -1684,6 +1719,7 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, InternalKey: revokeKey, }, SuccessTapLeaf: tapScriptTree.LeafMerkleProofs[0].TapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2060,6 +2096,14 @@ type CommitScriptTree struct { // RevocationLeaf is the leaf used to spend the output with the // revocation key signature. RevocationLeaf txscript.TapLeaf + + // AuxLeaf is an axuillaruy leaf that can be used to extend the base + // commitment script tree with new spend paths, or just as extra + // commitment space. When present, this leaf will always be in the + // left-most or right-most area of the tapscript tree. + // + // TODO(roasbeeF): need to ensure commitment position... + AuxLeaf fn.Option[txscript.TapLeaf] } // A compile time check to ensure CommitScriptTree implements the @@ -2120,7 +2164,8 @@ func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath, // NewLocalCommitScriptTree returns a new CommitScript tree that can be used to // create and spend the commitment output for the local party. func NewLocalCommitScriptTree(csvTimeout uint32, - selfKey, revokeKey *btcec.PublicKey) (*CommitScriptTree, error) { + selfKey, revokeKey *btcec.PublicKey, + auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { // First, we'll need to construct the tapLeaf that'll be our delay CSV // clause. @@ -2140,8 +2185,14 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // the two leaves, and then obtain a root from that. delayTapLeaf := txscript.NewBaseTapLeaf(delayScript) revokeTapLeaf := txscript.NewBaseTapLeaf(revokeScript) + + tapLeaves := []txscript.TapLeaf{delayTapLeaf, revokeTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + tapScriptTree := txscript.AssembleTaprootScriptTree( - delayTapLeaf, revokeTapLeaf, + tapLeaves..., ) tapScriptRoot := tapScriptTree.RootNode.TapHash() @@ -2160,6 +2211,7 @@ func NewLocalCommitScriptTree(csvTimeout uint32, }, SettleLeaf: delayTapLeaf, RevocationLeaf: revokeTapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2229,7 +2281,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { commitScriptTree, err := NewLocalCommitScriptTree( - csvTimeout, selfKey, revokeKey, + csvTimeout, selfKey, revokeKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -2558,7 +2610,7 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { // NewRemoteCommitScriptTree constructs a new script tree for the remote party // to sweep their funds after a hard coded 1 block delay. func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, -) (*CommitScriptTree, error) { + auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { // First, construct the remote party's tapscript they'll use to sweep // their outputs. @@ -2574,10 +2626,16 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, return nil, err } + tapLeaf := txscript.NewBaseTapLeaf(remoteScript) + + tapLeaves := []txscript.TapLeaf{tapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With this script constructed, we'll map that into a tapLeaf, then // make a new tapscript root from that. - tapLeaf := txscript.NewBaseTapLeaf(remoteScript) - tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...) tapScriptRoot := tapScriptTree.RootNode.TapHash() // Now that we have our root, we can arrive at the final output script @@ -2594,6 +2652,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, InternalKey: &TaprootNUMSKey, }, SettleLeaf: tapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2610,9 +2669,9 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // OP_CHECKSIG // 1 OP_CHECKSEQUENCEVERIFY OP_DROP func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, -) (*btcec.PublicKey, error) { + auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { - commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey) + commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey, auxLeaf) if err != nil { return nil, err } diff --git a/input/size_test.go b/input/size_test.go index 1f447cf89fa..6968e7d51f8 100644 --- a/input/size_test.go +++ b/input/size_test.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -851,7 +852,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} commitScriptTree, err := input.NewLocalCommitScriptTree( testCSVDelay, testKey.PubKey(), - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -885,7 +886,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} commitScriptTree, err := input.NewLocalCommitScriptTree( testCSVDelay, testKey.PubKey(), - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -919,7 +920,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} //nolint:lll commitScriptTree, err := input.NewRemoteCommitScriptTree( - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -986,6 +987,7 @@ var witnessSizeTests = []witnessSizeTest{ scriptTree, err := input.SecondLevelHtlcTapscriptTree( testKey.PubKey(), testCSVDelay, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1025,6 +1027,7 @@ var witnessSizeTests = []witnessSizeTest{ scriptTree, err := input.SecondLevelHtlcTapscriptTree( testKey.PubKey(), testCSVDelay, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1073,6 +1076,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1114,7 +1118,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1156,7 +1160,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1203,6 +1207,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1263,6 +1268,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1308,7 +1314,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1394,6 +1400,7 @@ func genTimeoutTx(t *testing.T, if chanType.IsTaproot() { tapscriptTree, err = input.SenderHTLCScriptTaproot( testPubkey, testPubkey, testPubkey, testHash160, false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1462,7 +1469,7 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx { if chanType.IsTaproot() { tapscriptTree, err = input.ReceiverHTLCScriptTaproot( testCLTVExpiry, testPubkey, testPubkey, testPubkey, - testHash160, false, + testHash160, false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) diff --git a/input/taproot_test.go b/input/taproot_test.go index 801b0fef4d5..25ff3e71e55 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -1,13 +1,16 @@ package input import ( + "bytes" "crypto/rand" + "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -31,7 +34,9 @@ type testSenderHtlcScriptTree struct { htlcAmt int64 } -func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { +func newTestSenderHtlcScriptTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testSenderHtlcScriptTree { + var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) require.NoError(t, err) @@ -48,7 +53,7 @@ func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { payHash := preImage.Hash() htlcScriptTree, err := SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, auxLeaf, ) require.NoError(t, err) @@ -207,13 +212,11 @@ func htlcSenderTimeoutWitnessGen(sigHash txscript.SigHashType, } } -// TestTaprootSenderHtlcSpend tests that all the positive and negative paths -// for the sender HTLC tapscript tree work as expected. -func TestTaprootSenderHtlcSpend(t *testing.T) { - t.Parallel() +func testTaprootSenderHtlcSpend(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { // First, create a new test script tree. - htlcScriptTree := newTestSenderHtlcScriptTree(t) + htlcScriptTree := newTestSenderHtlcScriptTree(t, auxLeaf) spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{}) @@ -432,6 +435,29 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { } } +// TestTaprootSenderHtlcSpend tests that all the positive and negative paths +// for the sender HTLC tapscript tree work as expected. +func TestTaprootSenderHtlcSpend(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootSenderHtlcSpend(t, auxLeaf) + }) + } +} + type testReceiverHtlcScriptTree struct { preImage lntypes.Preimage @@ -452,7 +478,9 @@ type testReceiverHtlcScriptTree struct { lockTime int32 } -func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { +func newTestReceiverHtlcScriptTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testReceiverHtlcScriptTree { + var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) require.NoError(t, err) @@ -471,7 +499,7 @@ func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { payHash := preImage.Hash() htlcScriptTree, err := ReceiverHTLCScriptTaproot( cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(), - revokeKey.PubKey(), payHash[:], false, + revokeKey.PubKey(), payHash[:], false, auxLeaf, ) require.NoError(t, err) @@ -629,15 +657,13 @@ func htlcReceiverSuccessWitnessGen(sigHash txscript.SigHashType, } } -// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an -// accepted HTLC (on the commitment transaction) of the receiver work properly. -func TestTaprootReceiverHtlcSpend(t *testing.T) { - t.Parallel() +func testTaprootReceiverHtlcSpend(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { // We'll start by creating the HTLC script tree (contains all 3 valid // spend paths), and also a mock spend transaction that we'll be // signing below. - htlcScriptTree := newTestReceiverHtlcScriptTree(t) + htlcScriptTree := newTestReceiverHtlcScriptTree(t, auxLeaf) // TODO(roasbeef): issue with revoke key??? ctrl block even/odd @@ -889,6 +915,30 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { assertEngineExecution(t, i, testCase.valid, newEngine) }) } + +} + +// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an +// accepted HTLC (on the commitment transaction) of the receiver work properly. +func TestTaprootReceiverHtlcSpend(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootReceiverHtlcSpend(t, auxLeaf) + }) + } } type testCommitScriptTree struct { @@ -905,7 +955,9 @@ type testCommitScriptTree struct { *CommitScriptTree } -func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { +func newTestCommitScriptTree(local bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*testCommitScriptTree, error) { + selfKey, err := btcec.NewPrivateKey() if err != nil { return nil, err @@ -925,10 +977,11 @@ func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { if local { commitScriptTree, err = NewLocalCommitScriptTree( csvDelay, selfKey.PubKey(), revokeKey.PubKey(), + auxLeaf, ) } else { commitScriptTree, err = NewRemoteCommitScriptTree( - selfKey.PubKey(), + selfKey.PubKey(), auxLeaf, ) } if err != nil { @@ -1020,12 +1073,10 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType, } } -// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming -// one's output after a force close behaves as expected. -func TestTaprootCommitScriptToSelf(t *testing.T) { - t.Parallel() +func testTaprootCommitScriptToSelf(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { - commitScriptTree, err := newTestCommitScriptTree(true) + commitScriptTree, err := newTestCommitScriptTree(true, auxLeaf) require.NoError(t, err) spendTx := wire.NewMsgTx(2) @@ -1187,6 +1238,29 @@ func TestTaprootCommitScriptToSelf(t *testing.T) { } } +// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming +// one's output after a force close behaves as expected. +func TestTaprootCommitScriptToSelf(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootCommitScriptToSelf(t, auxLeaf) + }) + } +} + func remoteCommitSweepWitGen(sigHash txscript.SigHashType, commitScriptTree *testCommitScriptTree) witnessGen { @@ -1220,12 +1294,9 @@ func remoteCommitSweepWitGen(sigHash txscript.SigHashType, } } -// TestTaprootCommitScriptRemote tests that the remote party can properly sweep -// their output after force close. -func TestTaprootCommitScriptRemote(t *testing.T) { - t.Parallel() +func testTaprootCommitScriptRemote(t *testing.T, auxLeaf fn.Option[txscript.TapLeaf]) { - commitScriptTree, err := newTestCommitScriptTree(false) + commitScriptTree, err := newTestCommitScriptTree(false, auxLeaf) require.NoError(t, err) spendTx := wire.NewMsgTx(2) @@ -1364,6 +1435,29 @@ func TestTaprootCommitScriptRemote(t *testing.T) { } } +// TestTaprootCommitScriptRemote tests that the remote party can properly sweep +// their output after force close. +func TestTaprootCommitScriptRemote(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootCommitScriptRemote(t, auxLeaf) + }) + } +} + type testAnchorScriptTree struct { sweepKey *btcec.PrivateKey @@ -1599,25 +1693,21 @@ type testSecondLevelHtlcTree struct { tapScriptRoot []byte } -func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { +func newTestSecondLevelHtlcTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testSecondLevelHtlcTree { + delayKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, err - } + require.NoError(t, err) revokeKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, err - } + require.NoError(t, err) const csvDelay = 6 scriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey.PubKey(), csvDelay, + delayKey.PubKey(), csvDelay, auxLeaf, ) - if err != nil { - return nil, err - } + require.NoError(t, err) tapScriptRoot := scriptTree.RootNode.TapHash() @@ -1626,9 +1716,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { ) pkScript, err := PayToTaprootScript(htlcKey) - if err != nil { - return nil, err - } + require.NoError(t, err) const amt = 100 @@ -1643,7 +1731,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { amt: amt, scriptTree: scriptTree, tapScriptRoot: tapScriptRoot[:], - }, nil + } } func secondLevelHtlcSuccessWitGen(sigHash txscript.SigHashType, @@ -1713,13 +1801,10 @@ func secondLevelHtlcRevokeWitnessgen(sigHash txscript.SigHashType, } } -// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly -// spend the second level HTLC script to resolve HTLCs. -func TestTaprootSecondLevelHtlcScript(t *testing.T) { - t.Parallel() +func testTaprootSecondLevelHtlcScript(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { - htlcScriptTree, err := newTestSecondLevelHtlcTree() - require.NoError(t, err) + htlcScriptTree := newTestSecondLevelHtlcTree(t, auxLeaf) spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{}) @@ -1879,3 +1964,26 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { }) } } + +// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly +// spend the second level HTLC script to resolve HTLCs. +func TestTaprootSecondLevelHtlcScript(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootSecondLevelHtlcScript(t, auxLeaf) + }) + } +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index a34384876dd..e9cfc1f954d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -26,6 +26,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -7095,6 +7096,7 @@ func newOutgoingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -7339,6 +7341,7 @@ func newIncomingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 43c9acc8f39..ca08314b109 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -237,6 +238,7 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, case chanType.IsTaproot(): return input.NewLocalCommitScriptTree( csvDelay, selfKey, revokeKey, + fn.None[txscript.TapLeaf](), ) // If we are the initiator of a leased channel, then we have an @@ -320,7 +322,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, // with the sole tap leaf enforcing the 1 CSV delay. case chanType.IsTaproot(): toRemoteScriptTree, err := input.NewRemoteCommitScriptTree( - remoteKey, + remoteKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, 0, err @@ -427,6 +429,7 @@ func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, case chanType.IsTaproot(): return input.TaprootSecondLevelScriptTree( revocationKey, delayKey, csvDelay, + fn.None[txscript.TapLeaf](), ) // If we are the initiator of a leased channel, then we have an @@ -1095,6 +1098,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // We're being paid via an HTLC by the remote party, and the HTLC is @@ -1104,6 +1108,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // We're sending an HTLC which is being added to our commitment @@ -1113,6 +1118,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // Finally, we're paying the remote party via an HTLC, which is being @@ -1122,6 +1128,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) } diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index 8b6c20194f1..7780239f07a 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -307,9 +308,11 @@ func newTaprootJusticeKit(sweepScript []byte, keyRing := breachInfo.KeyRing + // TODO(roasbeef): aux leaf tower updates needed + tree, err := input.NewLocalCommitScriptTree( breachInfo.RemoteDelay, keyRing.ToLocalKey, - keyRing.RevocationKey, + keyRing.RevocationKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -416,7 +419,9 @@ func (t *taprootJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, return nil, nil, 0, err } - scriptTree, err := input.NewRemoteCommitScriptTree(toRemotePk) + scriptTree, err := input.NewRemoteCommitScriptTree( + toRemotePk, fn.None[txscript.TapLeaf](), + ) if err != nil { return nil, nil, 0, err } diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index fd12993a0a7..5895b25447b 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -304,7 +305,9 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { name: "taproot commitment", blobType: TypeAltruistTaprootCommit, expWitnessScript: func(pk *btcec.PublicKey) []byte { - tree, _ := input.NewRemoteCommitScriptTree(pk) + tree, _ := input.NewRemoteCommitScriptTree( + pk, fn.None[txscript.TapLeaf](), + ) return tree.SettleLeaf.Script }, @@ -460,7 +463,7 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { rev *btcec.PublicKey) []byte { script, _ := input.NewLocalCommitScriptTree( - csvDelay, delay, rev, + csvDelay, delay, rev, fn.None[txscript.TapLeaf](), ) return script.RevocationLeaf.Script