From 5f16e8e7ce05f673f62207cad87eac6d2b955ab3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 19:57:03 -0800 Subject: [PATCH 01/18] input: add PayToTaprootScript helper func In this commit, we add a helper function to take a taproot output key and turn it into a v1 witness program. --- input/taproot.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/input/taproot.go b/input/taproot.go index 78294f03af3..935050f78be 100644 --- a/input/taproot.go +++ b/input/taproot.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" @@ -135,3 +136,15 @@ func TapscriptFullKeyOnly(taprootKey *btcec.PublicKey) *waddrmgr.Tapscript { FullOutputKey: taprootKey, } } + +// PayToTaprootScript creates a new script to pay to a version 1 (taproot) +// witness program. The passed public key will be serialized as an x-only key +// to create the witness program. +func PayToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + builder.AddOp(txscript.OP_1) + builder.AddData(schnorr.SerializePubKey(taprootKey)) + + return builder.Script() +} From f233976bd43f3d18eeea4779d4144aa4be42ecad Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:00:05 -0800 Subject: [PATCH 02/18] input: add GenTaprootFundingScript based on musig2 In this commit, we add GenTaprootFundingScript, which'll return the taproot pkScript and output for a taproot+musig2 channel. This uses musig2 key aggregation with sorting activated. The final key produced uses a bip86 tweak, meaning that the output key provably doesn't commit to any script path. In the future, we may want to permit this, as then it allows for a greater degree of programmability of the funding output. --- input/script_utils.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index d625e69dc03..442a2bf66f3 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -141,6 +142,40 @@ func GenFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, erro return witnessScript, wire.NewTxOut(amt, pkScript), nil } +// GenTaprootFundingScript constructs the taproot-native funding output that +// uses musig2 to create a single aggregated key to anchor the channel. +func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, + amt int64) ([]byte, *wire.TxOut, error) { + + // Similar to the existing p2wsh funding script, we'll always make sure + // we sort the keys before any major operations. In order to ensure + // that there's no other way this output can be spent, we'll use a BIP + // 86 tweak here during aggregation. + // + // TODO(roasbeef): revisit if BIP 86 is needed here? + combinedKey, _, _, err := musig2.AggregateKeys( + []*btcec.PublicKey{aPub, bPub}, true, + musig2.WithBIP86KeyTweak(), + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to combine keys: %w", err) + } + + // Now that we have the combined key, we can create a taproot pkScript + // from this, and then make the txout given the amount. + pkScript, err := PayToTaprootScript(combinedKey.FinalKey) + if err != nil { + return nil, nil, fmt.Errorf("unable to make taproot "+ + "pkscript: %w", err) + } + + txOut := wire.NewTxOut(amt, pkScript) + + // For the "witness program" we just return the raw pkScript since the + // output we create can _only_ be spent with a musig2 signature. + return pkScript, txOut, nil +} + // SpendMultiSig generates the witness stack required to redeem the 2-of-2 p2wsh // multi-sig output. func SpendMultiSig(witnessScript, pubA []byte, sigA Signature, From fbdc28ec3843501efdec8d494ae620ab1189c283 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:02:18 -0800 Subject: [PATCH 03/18] input: add TaprootCommitScriptToSelf for taproot to self script --- input/script_utils.go | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 442a2bf66f3..5e0fbade9da 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -6,6 +6,7 @@ import ( "fmt" "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/txscript" @@ -1059,6 +1060,62 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } +// TaprootCommitScriptToSelf creates the taproot witness program that commits +// to the revocation (keyspend) and delay path (script path) in a single +// taproot output key. +// +// For the delay path we have the following tapscript leaf script: +// +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY OP_DROP +// +// This can then be spent with just: +// +// +// +// Where the to_delay_script is listed above, and the delay_control_block +// computed as: +// +// delay_control_block = (output_key_y_parity | 0xc0) || revocationpubkey +// +// The revocation key spend path will simply present a valid signature with the +// witness being just: +// +// +func TaprootCommitScriptToSelf(csvTimeout uint32, + selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { + + // First, we'll need to construct the tapLeaf that'll be our delay CSV + // clause. + // + // TODO(roasbeef): extract into diff func + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(selfKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddInt64(int64(csvTimeout)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + delayScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With the delay script computed, we'll now create a tapscript tree + // with a single leaf, and then obtain a root from that. + tapLeaf := txscript.NewBaseTapLeaf(delayScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // Now that we have our root, we can arrive at the final output script + // by tweaking the internal key with this root. + toLocalOutputKey := txscript.ComputeTaprootOutputKey( + revokeKey, tapScriptRoot[:], + ) + + return toLocalOutputKey, nil +} + // LeaseCommitScriptToSelf constructs the public key script for the output on the // commitment transaction paying to the "owner" of said commitment transaction. // If the other party learns of the preimage to the revocation hash, then they From 578a16a64e37e498c5cb0d1d695053ccc0afbda8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:02:30 -0800 Subject: [PATCH 04/18] input: add TaprootCommitScriptToRemote for taproot to remote script --- input/script_utils.go | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 5e0fbade9da..3461924a954 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1324,6 +1324,54 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } +// TaprootCommitScriptToRemote constructs a taproot witness program for the +// output on the commitment transaction for the remote party. For the top level +// key spend, we'll use the combined funding key (musig2.KeyAgg(k1, k2)), as a +// sort of practical NUMs point (the local party would never sign for this). We +// then commit to a single tapscript leaf that holds the normal CSV 1 delay +// script. +// +// Our single tapleaf will use the following script: +// +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY +// +// The CSV clause is a bit subtle, but OP_CHECKSIG will return true if it +// succeeds, which then enforces our 1 CSV. The true will remain on the stack, +// causing the script to pass. If the CHECKSIG fails, then a 0 will remain on +// the stack. +// +// TODO(roasbeef): double check here can't pass additional stack elements? +func TaprootCommitScriptToRemote(combinedFundingKey, + remoteKey *btcec.PublicKey) (*btcec.PublicKey, error) { + + // First, construct the remote party's tapscript they'll use to sweep their + // outputs. + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(remoteKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + delayScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With this script constructed, we'll map that into a tapLeaf, then + // make a new tapscript root from that. + tapLeaf := txscript.NewBaseTapLeaf(delayScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // Now that we have our root, we can arrive at the final output script + // by tweaking the internal key with this root. + toRemoteOutputKey := txscript.ComputeTaprootOutputKey( + combinedFundingKey, tapScriptRoot[:], + ) + + return toRemoteOutputKey, nil +} + // LeaseCommitScriptToRemoteConfirmed constructs the script for the output on // the commitment transaction paying to the remote party of said commitment // transaction. The money can only be spend after one confirmation. From 99066d75cc1dba843d3a522f7105f6a546c0e255 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:02:44 -0800 Subject: [PATCH 05/18] input: add TaprootOutputKeyAnchor for taproot anchor outputs --- input/script_utils.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 3461924a954..b3eaeb33338 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1468,6 +1468,41 @@ func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } +// TaprootOutputKeyAnchor returns the segwit v1 (taproot) witness program that +// encodes the anchor output spending conditions: the passed key can be used +// for keyspend, with the OP_CSV 16 clause living within an internal tapscript +// leaf. +// +// Spend paths: +// - Key spend: +// - Script spend: OP_16 CSV +func TaprootOutputKeyAnchor(key *btcec.PublicKey) (*btcec.PublicKey, error) { + // The main script used is just a OP_16 CSV (anyone can sweep after 16 + // blocks). + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_16) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + anchorScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With the script, we can make our sole leaf, then derive the root + // from that. + tapLeaf := txscript.NewBaseTapLeaf(anchorScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // Now that we have our root, we can arrive at the final output script + // by tweaking the internal key with this root. + anchorKey := txscript.ComputeTaprootOutputKey( + key, tapScriptRoot[:], + ) + + return anchorKey, nil +} + // CommitSpendAnchor constructs a valid witness allowing a node to spend their // anchor output on the commitment transaction using their funding key. This is // used for the anchor channel type. From 84b36ec0685648d2564047ab7510976fc5946922 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:04:18 -0800 Subject: [PATCH 06/18] input: add tapscript utils for the sender HTLC script Unlike the old HTLC scripts, we now need to handle the various control block interactions. As is, we opt to simply re-compute the entire tree when needed, as the tree only has two leaves. --- input/script_utils.go | 157 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index b3eaeb33338..e2c22577045 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -479,6 +479,163 @@ func SenderHtlcSpendTimeout(receiverSig Signature, return witnessStack, nil } +// SenderHTLCTapLeafTimeout returns the full tapscript leaf for the timeout +// path of the sender HTLC. This is a small script that allows the sender to +// timeout the HTLC after a period of time: +// +// OP_CHECKSIGVERIFY +// OP_CHECKSIG +func SenderHTLCTapLeafTimeout(senderHtlcKey, + receiverHtlcKey *btcec.PublicKey) (txscript.TapLeaf, error) { + + builder := txscript.NewScriptBuilder() + + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + timeoutLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + return txscript.NewBaseTapLeaf(timeoutLeafScript), nil +} + +// SenderHTLCTapLeafSuccess returns the full tapscript leaf for the success +// path of the sender HTLC. This is a small script that allows the receiver to +// redeem the HTLC with a pre-image: +// +// OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 +// OP_EQUALVERIFY +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY +func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, + paymentHash []byte) (txscript.TapLeaf, error) { + + builder := txscript.NewScriptBuilder() + + // Check that the pre-image is 32 bytes as required. + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUALVERIFY) + + // Check that the specified pre-images matches what we hard code into + // the script. + builder.AddOp(txscript.OP_HASH160) + builder.AddData(Ripemd160H(paymentHash)) + builder.AddOp(txscript.OP_EQUALVERIFY) + + // Verify the remote party's signature, then make them wait 1 block + // after confirmation to properly sweep. + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + successLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + return txscript.NewBaseTapLeaf(successLeafScript), nil +} + +// HtlcScriptTree... +type HtlcScriptTree struct { + // TaprootKey... + TaprootKey *btcec.PublicKey + + // SuccessTapLeaf... + SuccessTapLeaf txscript.TapLeaf + + // TimeoutTapLeaf... + TimeoutTapLeaf txscript.TapLeaf + + // TapscriptTree... + TapscriptTree *txscript.IndexedTapScriptTree +} + +// senderHtlcTapScriptTree builds the tapscript tree which is used to anchor +// the HTLC key for HTLCs on the sender's commitment. +func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, + revokeKey *btcec.PublicKey, payHash []byte) (*HtlcScriptTree, error) { + + // First, we'll obtain the tap leaves for both the success and timeout + // path. + successTapLeaf, err := SenderHTLCTapLeafSuccess( + receiverHtlcKey, payHash, + ) + if err != nil { + return nil, err + } + timeoutTapLeaf, err := SenderHTLCTapLeafTimeout( + senderHtlcKey, receiverHtlcKey, + ) + if err != nil { + return nil, err + } + + // With the two leaves obtained, we'll now make the tapscript tree, + // then obtain the root from that + tapscriptTree := txscript.AssembleTaprootScriptTree( + successTapLeaf, timeoutTapLeaf, + ) + + tapScriptRoot := tapscriptTree.RootNode.TapHash() + + // With the tapscript root obtained, we'll tweak the revocation key + // with this value to obtain the key that HTLCs will be sent to. + htlcKey := txscript.ComputeTaprootOutputKey( + revokeKey, tapScriptRoot[:], + ) + + return &HtlcScriptTree{ + TaprootKey: htlcKey, + SuccessTapLeaf: successTapLeaf, + TimeoutTapLeaf: timeoutTapLeaf, + TapscriptTree: tapscriptTree, + }, nil +} + +// SenderHTLCScriptTaproot constructs the taproot witness program (schnorr key) +// for an outgoing HTLC on the sender's version of the commitment transaction. +// This method returns the top level tweaked public key that commits to both +// the script paths. +// +// The returned key commits to a tapscript tree with two possible paths: +// +// - Timeout path: +// OP_CHECKSIGVERIFY +// OP_CHECKSIG +// +// - Success path: +// OP_SIZE 32 OP_EQUALVERIFY +// OP_HASH160 OP_EQUALVERIFY +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY +// +// The timeout path can be spent with a witness of (sender timeout): +// +// +// +// The success path can be spent with a valid control block, and a witness of +// (receiver redeem): +// +// +// +// The top level keyspend key is the revocation key, which allows a defender to +// unilaterally spend the created output. +func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, + revokeKey *btcec.PublicKey, payHash []byte) (*HtlcScriptTree, error) { + + // Given all the necessary parameters, we'll return the HTLC script + // tree that includes the top level output script, as well as the two + // tap leaf paths. + return senderHtlcTapScriptTree( + senderHtlcKey, receiverHtlcKey, revokeKey, payHash, + ) +} // ReceiverHTLCScript constructs the public key script for an incoming HTLC // output payment for the receiver's version of the commitment transaction. The // possible execution paths from this script include: From 8eb94903eaad7c2a6845ce39a774d5b0550dc3e1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:04:34 -0800 Subject: [PATCH 07/18] input: add spending funcs for taproot sender HTLCs --- input/script_utils.go | 92 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index e2c22577045..5959d469359 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -636,6 +636,98 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, senderHtlcKey, receiverHtlcKey, revokeKey, payHash, ) } + +// SenderHTLCScriptTaprootRedeem creates a valid witness needed to redeem a +// sender taproot HTLC with the pre-image. The returned witness is valid and +// includes the control block required to spend the output. +func SenderHTLCScriptTaprootRedeem(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx, preimage []byte, revokeKey *btcec.PublicKey, + tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // In addition to the signature and the witness/leaf script, we also + // need to make a control block proof using the tapscript tree. + successTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + successIdx := tapscriptTree.LeafProofIndex[successTapLeafHash] + successMerkleProof := tapscriptTree.LeafMerkleProofs[successIdx] + successControlBlock := successMerkleProof.ToControlBlock(revokeKey) + + // The final witness stack is: + // + witnessStack := make(wire.TxWitness, 4) + witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[1] = preimage + witnessStack[2] = signDesc.WitnessScript + witnessStack[4], err = successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + +// SenderHTLCScriptTaprootTimeout creates a valid witness needed to timeout an +// HTLC on the sender's commitment transaction. The returned witness is valid and +// includes the control block required to spend the output. +func SenderHTLCScriptTaprootTimeout(receiverSig Signature, + receiverSigHash txscript.SigHashType, signer Signer, + signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx, + revokeKey *btcec.PublicKey, + tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + sweepSig, err := signer.SignOutputRaw(htlcTimeoutTx, signDesc) + if err != nil { + return nil, err + } + + // With the sweep signature obtained, we'll obtain the control block + // proof needed to perform a valid spend for the timeout path. + timeoutTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + timeoutIdx := tapscriptTree.LeafProofIndex[timeoutTapLeafHash] + timeoutMerkleProof := tapscriptTree.LeafMerkleProofs[timeoutIdx] + timeoutControlBlock := timeoutMerkleProof.ToControlBlock(revokeKey) + + // The final witness stack is: + // + witnessStack := make(wire.TxWitness, 4) + witnessStack[0] = append(receiverSig.Serialize(), byte(receiverSigHash)) + witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[2] = signDesc.WitnessScript + witnessStack[3], err = timeoutControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + +// SenderHTLCScriptTaprootRevoke creates a valid witness needed to spend the +// revocation path of the HTLC. This uses a plain keyspend using the specified +// revocation key. +func SenderHTLCScriptTaprootRevoke(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // The witness stack in this case is pretty simple: we only need to + // specify the signature generated. + witnessStack := make(wire.TxWitness, 1) + witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + + return witnessStack, nil +} + // ReceiverHTLCScript constructs the public key script for an incoming HTLC // output payment for the receiver's version of the commitment transaction. The // possible execution paths from this script include: From 5c10f280b09e4c89ec083dfc5e46416e4c86ad32 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:04:56 -0800 Subject: [PATCH 08/18] input: add taproot script funcs for receiver HTLCs --- input/script_utils.go | 155 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 5959d469359..7a2a5f14252 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1000,6 +1000,161 @@ func ReceiverHtlcSpendTimeout(signer Signer, signDesc *SignDescriptor, return witnessStack, nil } +// ReceiverHtlcTapLeafTimeout returns the full tapscript leaf for the timeout +// path of the sender HTLC. This is a small script that allows the sender +// timeout the HTLC after expiry: +// +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY +// OP_CHECKLOCKTIMEVERIFY OP_DROP +func ReceiverHtlcTapLeafTimeout(receiverHtlcKey *btcec.PublicKey, + cltvExpiry uint32) (txscript.TapLeaf, error) { + + builder := txscript.NewScriptBuilder() + + // The first part of the script will verify a signature from the + // receiver authorizing the spend. + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + // The second portion will ensure that the CLTV expiry on the spending + // transaction is correct. + builder.AddInt64(int64(cltvExpiry)) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + builder.AddOp(txscript.OP_DROP) + + timeoutLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, nil + } + + return txscript.NewBaseTapLeaf(timeoutLeafScript), nil +} + +// ReceiverHtlcTapLeafSuccess returns the full tapscript leaf for the success +// path for an HTLC on the receiver's commitment transaction. This script +// allows the receiver to redeem an HTLC with knowledge of the preimage: +// +// OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 +// OP_EQUALVERIFY +// OP_CHECKSIGVERIFY +// OP_CHECKSIG +func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, + senderHtlcKey *btcec.PublicKey, + paymentHash []byte) (txscript.TapLeaf, error) { + + builder := txscript.NewScriptBuilder() + + // Check that the pre-image is 32 bytes as required. + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUALVERIFY) + + // Check that the specified pre-images matches what we hard code into + // the script. + builder.AddOp(txscript.OP_HASH160) + builder.AddData(Ripemd160H(paymentHash)) + builder.AddOp(txscript.OP_EQUALVERIFY) + + // Verify the "2-of-2" multi-sig that requires both parties to sign + // off. + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + successLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + return txscript.NewBaseTapLeaf(successLeafScript), nil +} + +// receiverHtlcTapScriptTree builds the tapscript tree which is used to anchor +// the HTLC key for HTLCs on the receiver's commitment. +func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, + revokeKey *btcec.PublicKey, payHash []byte, + cltvExpiry uint32) (*HtlcScriptTree, error) { + + // First, we'll obtain the tap leaves for both the success and timeout + // path. + successTapLeaf, err := ReceiverHtlcTapLeafSuccess( + receiverHtlcKey, senderHtlcKey, payHash, + ) + if err != nil { + return nil, err + } + timeoutTapLeaf, err := ReceiverHtlcTapLeafTimeout( + receiverHtlcKey, cltvExpiry, + ) + if err != nil { + return nil, err + } + + // With the two leaves obtained, we'll now make the tapscript tree, + // then obtain the root from that + tapscriptTree := txscript.AssembleTaprootScriptTree( + successTapLeaf, timeoutTapLeaf, + ) + + tapScriptRoot := tapscriptTree.RootNode.TapHash() + + // With the tapscript root obtained, we'll tweak the revocation key + // with this value to obtain the key that HTLCs will be sent to. + htlcKey := txscript.ComputeTaprootOutputKey( + revokeKey, tapScriptRoot[:], + ) + + return &HtlcScriptTree{ + TaprootKey: htlcKey, + SuccessTapLeaf: successTapLeaf, + TimeoutTapLeaf: timeoutTapLeaf, + TapscriptTree: tapscriptTree, + }, nil +} + +// ReceiverHTLCScriptTaproot cosntructs the taproot witness program (schnor +// key) for an outgoing HTLC on the receiver's version of the commitment +// transaction. This method returns the top level tweaked public key that +// commits to both the script paths. +// +// The returned key commits to a tapscript tree with two possible paths: +// +// - The timeout path: +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY +// OP_CHECKLOCKTIMEVERIFY OP_DROP +// +// - Success path: +// OP_SIZE 32 OP_EQUALVERIFY +// OP_HASH160 OP_EQUALVERIFY +// OP_CHECKSIGVERIFY +// OP_CHECKSIG +// +// The timeout path can be be spent with a witness of: +// - +// +// The success path can be spent with a witness of: +// - +// +// The top level keyspend key is the revocation key, which allows a defender to +// unilaterally spend the created output. Both the final output key as well as +// the tap leaf are returned. +func ReceiverHTLCScriptTaproot(cltvExpiry uint32, + senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, + payHash []byte) (*HtlcScriptTree, error) { + + // Given all the necessary parameters, we'll return the HTLC script + // tree that includes the top level output script, as well as the two + // tap leaf paths. + return receiverHtlcTapScriptTree( + senderHtlcKey, receiverHtlcKey, revocationKey, payHash, + cltvExpiry, + ) +} + // SecondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction act as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be From 006113a6ab661bdff4aa92d366d53aec628fe9a0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:05:15 -0800 Subject: [PATCH 09/18] input: add spending funcs for taproot receiver HTLC ctrl blocks --- input/script_utils.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 7a2a5f14252..2b3b74cd456 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1155,6 +1155,97 @@ func ReceiverHTLCScriptTaproot(cltvExpiry uint32, ) } +// ReceiverHTLCScriptTaprootRedeem creates a valid witness needed to redeem a +// receiver taproot HTLC with the pre-image. The returned witness is valid and +// includes the control block required to spend the output. +func ReceiverHTLCScriptTaprootRedeem(senderSig Signature, + senderSigHash txscript.SigHashType, paymentPreimage []byte, + signer Signer, signDesc *SignDescriptor, + htlcSuccessTx *wire.MsgTx, revokeKey *btcec.PublicKey, + tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // First, we'll generate a signature for the HTLC success transaction. + // The signDesc should be signing with the public key used as the + // receiver's public key and also the correct single tweak. + sweepSig, err := signer.SignOutputRaw(htlcSuccessTx, signDesc) + if err != nil { + return nil, err + } + + // In addition to the signature and the witness/leaf script, we also + // need to make a control block proof using the tapscript tree. + successTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + successIdx := tapscriptTree.LeafProofIndex[successTapLeafHash] + successMerkleProof := tapscriptTree.LeafMerkleProofs[successIdx] + successControlBlock := successMerkleProof.ToControlBlock(revokeKey) + + // The final witness stack is: + // * + witnessStack := wire.TxWitness(make([][]byte, 5)) + witnessStack[0] = append(senderSig.Serialize(), byte(senderSigHash)) + witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[2] = paymentPreimage + witnessStack[3] = signDesc.WitnessScript + witnessStack[4], err = successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + +// ReceiverHtlcTapLeafTimeout creates a valid witness needed to timeout an HTLC +// on the receiver's commitment transaction after the timeout has elapsed +func ReceiverHTLCScriptTaprootTimeout(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx, cltvExpiry int32, revokeKey *btcec.PublicKey, + tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // If the caller set a proper timeout value, then we'll apply it + // directly to the transaction. + // + // TODO(roasbeef): helper func + if cltvExpiry != -1 { + // The HTLC output has an absolute time period before we are + // permitted to recover the pending funds. Therefore we need to + // set the locktime on this sweeping transaction in order to + // pass Script verification. + sweepTx.LockTime = uint32(cltvExpiry) + } + + // With the lock time on the transaction set, we'll now generate a + // signature for the sweep transaction. The passed sign descriptor + // should be created using the raw public key of the sender (w/o the + // single tweak applied), and the single tweak set to the proper value + // taking into account the current state's point. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // In addition to the signature and the witness/leaf script, we also + // need to make a control block proof using the tapscript tree. + successTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + successIdx := tapscriptTree.LeafProofIndex[successTapLeafHash] + successMerkleProof := tapscriptTree.LeafMerkleProofs[successIdx] + successControlBlock := successMerkleProof.ToControlBlock(revokeKey) + + // The final witness is pretty simple, we just need to present a valid + // signature for the script, and then provide the control block. + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + // SecondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction act as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be From 3e59b1bf184de7da84d60ceaa7bd6b818519f29a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:05:40 -0800 Subject: [PATCH 10/18] input: add taproot second level HTLC scripts --- input/script_utils.go | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 2b3b74cd456..978f0f7cbd0 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1315,6 +1315,96 @@ func SecondLevelHtlcScript(revocationKey, delayKey *btcec.PublicKey, return builder.Script() } +// TODO(roasbeef): move all taproot stuff to new file? + +// TaprootSecondLevelTapLeaf constructs the tap leaf used as the sole script +// path for a second level HTLC spend. +// +// The final script used is: +// +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY OP_DROP +func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey, + csvDelay uint32) (txscript.TapLeaf, error) { + + builder := txscript.NewScriptBuilder() + + // Ensure the proper party can sign for this output. + builder.AddData(schnorr.SerializePubKey(delayKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + // Assuming the above passes, then we'll now ensure that the CSV delay + // has been upheld, dropping the int we pushed on. If the sig above is + // valid, then a 1 will be left on the stack. + builder.AddInt64(int64(csvDelay)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + secondLevelLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + return txscript.NewBaseTapLeaf(secondLevelLeafScript), nil +} + +// 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) { + + // First grab the second level leaf script we need to create the top level + // output. + secondLevelTapLeaf, err := TaprootSecondLevelTapLeaf(delayKey, csvDelay) + if err != nil { + return nil, err + } + + // 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 +} + +// TaprootSecondLevelHtlcScript is the uniform script that's used as the output +// for the second-level HTLC transaction. The second level transaction acts as +// an off-chain 2-of-2 covenant that can only be spent a particular way and to +// a particular output. +// +// Possible Input Scripts: +// - revocation sig +// - +// +// The script main script lets the broadcaster spend after a delay the script +// path: +// +// OP_CHECKSIG +// OP_CHECKSEQUENCEVERIFY OP_DROP +// +// The keyspend path require knowledge of the top level revocation private key. +func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, + csvDelay uint32) (*btcec.PublicKey, error) { + + // First, we'll make the tapscript tree that commits to the redemption + // path. + tapScriptTree, err := SecondLevelHtlcTapscriptTree( + delayKey, csvDelay, + ) + if err != nil { + return nil, err + } + + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // With the tapscript root obtained, we'll tweak the revocation key + // with this value to obtain the key that the second level spend will + // create. + redemptionKey := txscript.ComputeTaprootOutputKey( + revokeKey, tapScriptRoot[:], + ) + + return redemptionKey, nil +} + // LeaseSecondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction acts as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be From 9a5151faec4e8a539a80a4a6eca937bd79d71a98 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 16 Jan 2023 20:06:03 -0800 Subject: [PATCH 11/18] input: add spending funcs for second level HTLC tapscript ctrl blocks --- input/script_utils.go | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/input/script_utils.go b/input/script_utils.go index 978f0f7cbd0..62d954d47f9 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1405,6 +1405,73 @@ func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, return redemptionKey, nil } +// TaprootHtlcSpendRevoke spends a second-level HTLC output via the revocation +// path. This uses the top level keyspend path to redeem the contested output. +// +// The passed SignDescriptor MUST have the proper witness script and also the +// proper top-level tweak derived from the tapscript tree for the second level +// output. +func TaprootHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, + revokeTx *wire.MsgTx) (wire.TxWitness, error) { + + // We don't need any spacial modifications to the transaction as this + // is just sweeping a revoked HTLC output. So we'll generate a regular + // schnorr signature. + sweepSig, err := signer.SignOutputRaw(revokeTx, signDesc) + if err != nil { + return nil, err + } + + // The witness stack in this case is pretty simple: we only need to + // specify the signature generated. + witnessStack := make(wire.TxWitness, 1) + witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + + return witnessStack, nil +} + +// TaprootHtlcSpendSuccess spends a second-level HTLC output via the redemption +// path. This should be used to sweep funds after the pre-image is known or the +// timeout has elapsed on the commitment transaction of the broadcaster. +// +// NOTE: The caller MUST set the txn version, sequence number, and sign +// descriptor's sig hash cache before invocation. +func TaprootHtlcSpendSuccess(signer Signer, signDesc *SignDescriptor, + revokeKey *btcec.PublicKey, sweepTx *wire.MsgTx, + tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // First, we'll generate the sweep signature based on the populated + // sign desc. This should give us a valid schnorr signature for the + // sole script path leaf. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // Now that we have the sweep signature, we'll construct the control + // block needed to spend the script path. + redeemTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + redeemIdx := tapscriptTree.LeafProofIndex[redeemTapLeafHash] + redeemMerkleProof := tapscriptTree.LeafMerkleProofs[redeemIdx] + redeemControlBlock := redeemMerkleProof.ToControlBlock(revokeKey) + + // Now that we have the redeem control block, we can construct the + // final witness needed to spend the script: + // + // + witnessStack := make(wire.TxWitness, 3) + witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[2] = signDesc.WitnessScript + witnessStack[3], err = redeemControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + // LeaseSecondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction acts as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be From 9485cc06654306b4ee4566a8590f340ebc6a5da6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 24 Jan 2023 20:40:42 -0800 Subject: [PATCH 12/18] input: add new maybeAppendSighashType helper func --- input/script_utils.go | 77 ++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 62d954d47f9..1e64b069eab 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -521,7 +521,7 @@ func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, builder.AddInt64(32) builder.AddOp(txscript.OP_EQUALVERIFY) - // Check that the specified pre-images matches what we hard code into + // Check that the specified pre-image matches what we hard code into // the script. builder.AddOp(txscript.OP_HASH160) builder.AddData(Ripemd160H(paymentHash)) @@ -637,9 +637,21 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, ) } +// maybeAppendSighashType appends a sighash type to the end of a signature if +// the sighash type isn't sighash default. +func maybeAppendSighash(sig Signature, sigHash txscript.SigHashType) []byte { + sigBytes := sig.Serialize() + if sigHash != txscript.SigHashDefault { + return sigBytes + } + + return append(sigBytes, byte(sigHash)) +} + // SenderHTLCScriptTaprootRedeem creates a valid witness needed to redeem a // sender taproot HTLC with the pre-image. The returned witness is valid and -// includes the control block required to spend the output. +// includes the control block required to spend the output. This is the offered +// HTLC claimed by the remote party. func SenderHTLCScriptTaprootRedeem(signer Signer, signDesc *SignDescriptor, sweepTx *wire.MsgTx, preimage []byte, revokeKey *btcec.PublicKey, tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { @@ -659,12 +671,12 @@ func SenderHTLCScriptTaprootRedeem(signer Signer, signDesc *SignDescriptor, successControlBlock := successMerkleProof.ToControlBlock(revokeKey) // The final witness stack is: - // + // witnessStack := make(wire.TxWitness, 4) - witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[1] = preimage witnessStack[2] = signDesc.WitnessScript - witnessStack[4], err = successControlBlock.ToBytes() + witnessStack[3], err = successControlBlock.ToBytes() if err != nil { return nil, err } @@ -673,8 +685,9 @@ func SenderHTLCScriptTaprootRedeem(signer Signer, signDesc *SignDescriptor, } // SenderHTLCScriptTaprootTimeout creates a valid witness needed to timeout an -// HTLC on the sender's commitment transaction. The returned witness is valid and -// includes the control block required to spend the output. +// HTLC on the sender's commitment transaction. The returned witness is valid +// and includes the control block required to spend the output. This is a +// timeout of the offered HTLC by the sender. func SenderHTLCScriptTaprootTimeout(receiverSig Signature, receiverSigHash txscript.SigHashType, signer Signer, signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx, @@ -698,8 +711,8 @@ func SenderHTLCScriptTaprootTimeout(receiverSig Signature, // The final witness stack is: // witnessStack := make(wire.TxWitness, 4) - witnessStack[0] = append(receiverSig.Serialize(), byte(receiverSigHash)) - witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(receiverSig, receiverSigHash) + witnessStack[1] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[2] = signDesc.WitnessScript witnessStack[3], err = timeoutControlBlock.ToBytes() if err != nil { @@ -723,7 +736,7 @@ func SenderHTLCScriptTaprootRevoke(signer Signer, signDesc *SignDescriptor, // The witness stack in this case is pretty simple: we only need to // specify the signature generated. witnessStack := make(wire.TxWitness, 1) - witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) return witnessStack, nil } @@ -1004,17 +1017,17 @@ func ReceiverHtlcSpendTimeout(signer Signer, signDesc *SignDescriptor, // path of the sender HTLC. This is a small script that allows the sender // timeout the HTLC after expiry: // -// OP_CHECKSIG +// OP_CHECKSIG // OP_CHECKSEQUENCEVERIFY // OP_CHECKLOCKTIMEVERIFY OP_DROP -func ReceiverHtlcTapLeafTimeout(receiverHtlcKey *btcec.PublicKey, +func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, cltvExpiry uint32) (txscript.TapLeaf, error) { builder := txscript.NewScriptBuilder() // The first part of the script will verify a signature from the - // receiver authorizing the spend. - builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + // sender authorizing the spend (the timeout). + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) builder.AddOp(txscript.OP_CHECKSIG) builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) @@ -1051,7 +1064,7 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, builder.AddInt64(32) builder.AddOp(txscript.OP_EQUALVERIFY) - // Check that the specified pre-images matches what we hard code into + // Check that the specified pre-image matches what we hard code into // the script. builder.AddOp(txscript.OP_HASH160) builder.AddData(Ripemd160H(paymentHash)) @@ -1059,9 +1072,9 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // Verify the "2-of-2" multi-sig that requires both parties to sign // off. - builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) - builder.AddOp(txscript.OP_CHECKSIGVERIFY) builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) builder.AddOp(txscript.OP_CHECKSIG) successLeafScript, err := builder.Script() @@ -1115,10 +1128,11 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, }, nil } -// ReceiverHTLCScriptTaproot cosntructs the taproot witness program (schnor +// ReceiverHTLCScriptTaproot constructs the taproot witness program (schnor // key) for an outgoing HTLC on the receiver's version of the commitment // transaction. This method returns the top level tweaked public key that -// commits to both the script paths. +// commits to both the script paths. From the PoV fo teh receiver, this is an +// accepted HTLC. // // The returned key commits to a tapscript tree with two possible paths: // @@ -1134,10 +1148,10 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // OP_CHECKSIG // // The timeout path can be be spent with a witness of: -// - +// - // // The success path can be spent with a witness of: -// - +// - // // The top level keyspend key is the revocation key, which allows a defender to // unilaterally spend the created output. Both the final output key as well as @@ -1196,8 +1210,9 @@ func ReceiverHTLCScriptTaprootRedeem(senderSig Signature, return witnessStack, nil } -// ReceiverHtlcTapLeafTimeout creates a valid witness needed to timeout an HTLC -// on the receiver's commitment transaction after the timeout has elapsed +// ReceiverHTLCScriptTaprootTimeout creates a valid witness needed to timeout +// an HTLC on the receiver's commitment transaction after the timeout has +// elapsed. func ReceiverHTLCScriptTaprootTimeout(signer Signer, signDesc *SignDescriptor, sweepTx *wire.MsgTx, cltvExpiry int32, revokeKey *btcec.PublicKey, tapscriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { @@ -1226,19 +1241,19 @@ func ReceiverHTLCScriptTaprootTimeout(signer Signer, signDesc *SignDescriptor, // In addition to the signature and the witness/leaf script, we also // need to make a control block proof using the tapscript tree. - successTapLeafHash := txscript.NewBaseTapLeaf( + timeoutTapLeafHash := txscript.NewBaseTapLeaf( signDesc.WitnessScript, ).TapHash() - successIdx := tapscriptTree.LeafProofIndex[successTapLeafHash] - successMerkleProof := tapscriptTree.LeafMerkleProofs[successIdx] - successControlBlock := successMerkleProof.ToControlBlock(revokeKey) + timeoutIdx := tapscriptTree.LeafProofIndex[timeoutTapLeafHash] + timeoutMerkleProof := tapscriptTree.LeafMerkleProofs[timeoutIdx] + timeoutControlBlock := timeoutMerkleProof.ToControlBlock(revokeKey) // The final witness is pretty simple, we just need to present a valid // signature for the script, and then provide the control block. witnessStack := make(wire.TxWitness, 3) witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) witnessStack[1] = signDesc.WitnessScript - witnessStack[2], err = successControlBlock.ToBytes() + witnessStack[2], err = timeoutControlBlock.ToBytes() if err != nil { return nil, err } @@ -1462,9 +1477,9 @@ func TaprootHtlcSpendSuccess(signer Signer, signDesc *SignDescriptor, // // witnessStack := make(wire.TxWitness, 3) - witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) - witnessStack[2] = signDesc.WitnessScript - witnessStack[3], err = redeemControlBlock.ToBytes() + witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = redeemControlBlock.ToBytes() if err != nil { return nil, err } From 110c29a491628a81dba1b0fad4341570a3c2927d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sat, 4 Feb 2023 15:09:32 +0200 Subject: [PATCH 13/18] input: add exhaustive unit tests for new taproot scripts --- input/script_utils.go | 396 ++++++-- input/script_utils_test.go | 9 +- input/taproot_test.go | 1877 ++++++++++++++++++++++++++++++++++++ input/test_utils.go | 67 +- 4 files changed, 2277 insertions(+), 72 deletions(-) create mode 100644 input/taproot_test.go diff --git a/input/script_utils.go b/input/script_utils.go index 1e64b069eab..8ced5d36368 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -541,19 +541,24 @@ func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, return txscript.NewBaseTapLeaf(successLeafScript), nil } -// HtlcScriptTree... +// HtlcScriptTree holds the taproot output key, as well as the two script path +// leaves that every taproot HTLC script depends on. type HtlcScriptTree struct { - // TaprootKey... + // TaprootKey is the key that will be used to generate the taproot output. TaprootKey *btcec.PublicKey - // SuccessTapLeaf... + // SuccessTapLeaf is the tapleaf for the redemption path. SuccessTapLeaf txscript.TapLeaf - // TimeoutTapLeaf... + // TimeoutTapLeaf is the tapleaf for the timeout path. TimeoutTapLeaf txscript.TapLeaf - // TapscriptTree... + // TapscriptTree is the full tapscript tree that also includes the + // control block needed to spend each of the leaves. TapscriptTree *txscript.IndexedTapScriptTree + + // TapscriptTreeRoot is the root hash of the tapscript tree. + TapscriptRoot []byte } // senderHtlcTapScriptTree builds the tapscript tree which is used to anchor @@ -595,6 +600,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, TapscriptTree: tapscriptTree, + TapscriptRoot: tapScriptRoot[:], }, nil } @@ -641,7 +647,7 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, // the sighash type isn't sighash default. func maybeAppendSighash(sig Signature, sigHash txscript.SigHashType) []byte { sigBytes := sig.Serialize() - if sigHash != txscript.SigHashDefault { + if sigHash == txscript.SigHashDefault { return sigBytes } @@ -1051,8 +1057,8 @@ func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, // // OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 // OP_EQUALVERIFY -// OP_CHECKSIGVERIFY -// OP_CHECKSIG +// OP_CHECKSIGVERIFY +// OP_CHECKSIG func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, senderHtlcKey *btcec.PublicKey, paymentHash []byte) (txscript.TapLeaf, error) { @@ -1100,7 +1106,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, return nil, err } timeoutTapLeaf, err := ReceiverHtlcTapLeafTimeout( - receiverHtlcKey, cltvExpiry, + senderHtlcKey, cltvExpiry, ) if err != nil { return nil, err @@ -1125,6 +1131,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, TapscriptTree: tapscriptTree, + TapscriptRoot: tapScriptRoot[:], }, nil } @@ -1198,8 +1205,8 @@ func ReceiverHTLCScriptTaprootRedeem(senderSig Signature, // The final witness stack is: // * witnessStack := wire.TxWitness(make([][]byte, 5)) - witnessStack[0] = append(senderSig.Serialize(), byte(senderSigHash)) - witnessStack[1] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(senderSig, senderSigHash) + witnessStack[1] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[2] = paymentPreimage witnessStack[3] = signDesc.WitnessScript witnessStack[4], err = successControlBlock.ToBytes() @@ -1251,7 +1258,7 @@ func ReceiverHTLCScriptTaprootTimeout(signer Signer, signDesc *SignDescriptor, // The final witness is pretty simple, we just need to present a valid // signature for the script, and then provide the control block. witnessStack := make(wire.TxWitness, 3) - witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[1] = signDesc.WitnessScript witnessStack[2], err = timeoutControlBlock.ToBytes() if err != nil { @@ -1261,6 +1268,25 @@ func ReceiverHTLCScriptTaprootTimeout(signer Signer, signDesc *SignDescriptor, return witnessStack, nil } +// ReceiverHTLCScriptTaprootRevoke creates a valid witness needed to spend the +// revocation path of the HTLC from the PoV of the sender (offerer) of the +// HTLC. This uses a plain keyspend using the specified revocation key. +func ReceiverHTLCScriptTaprootRevoke(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // The witness stack in this case is pretty simple: we only need to + // specify the signature generated. + witnessStack := make(wire.TxWitness, 1) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + + return witnessStack, nil +} + // SecondLevelHtlcScript is the uniform script that's used as the output for // the second-level HTLC transactions. The second level transaction act as a // sort of covenant, ensuring that a 2-of-2 multi-sig output can only be @@ -1440,7 +1466,7 @@ func TaprootHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, // The witness stack in this case is pretty simple: we only need to // specify the signature generated. witnessStack := make(wire.TxWitness, 1) - witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) return witnessStack, nil } @@ -1477,7 +1503,7 @@ func TaprootHtlcSpendSuccess(signer Signer, signDesc *SignDescriptor, // // witnessStack := make(wire.TxWitness, 3) - witnessStack[0] = append(sweepSig.Serialize(), byte(signDesc.HashType)) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[1] = signDesc.WitnessScript witnessStack[2], err = redeemControlBlock.ToBytes() if err != nil { @@ -1727,6 +1753,66 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } +// CommitScript holds the taproot output key (in this case the revocation key, +// or a NUMs point for the remote output) along with the tapscript leaf that +// can spend the output after a delay. +type CommitScriptTree struct { + // TaprootKey is the key that will be used to generate the taproot + // output. + TaprootKey *btcec.PublicKey + + // SettleLeaf is the leaf used to settle the output after the delay. + SettleLeaf txscript.TapLeaf + + // TapscriptTree is the full tapscript tree that also includes the + // control block needed to spend each of the leaves. + TapscriptTree *txscript.IndexedTapScriptTree + + // TapscriptTreeRoot is the root hash of the tapscript tree. + TapscriptRoot []byte +} + +// 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) { + + // First, we'll need to construct the tapLeaf that'll be our delay CSV + // clause. + // + // TODO(roasbeef): extract into diff func + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(selfKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddInt64(int64(csvTimeout)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + delayScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With the delay script computed, we'll now create a tapscript tree + // with a single leaf, and then obtain a root from that. + tapLeaf := txscript.NewBaseTapLeaf(delayScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // Now that we have our root, we can arrive at the final output script + // by tweaking the internal key with this root. + toLocalOutputKey := txscript.ComputeTaprootOutputKey( + revokeKey, tapScriptRoot[:], + ) + + return &CommitScriptTree{ + SettleLeaf: tapLeaf, + TaprootKey: toLocalOutputKey, + TapscriptTree: tapScriptTree, + TapscriptRoot: tapScriptRoot[:], + }, nil +} + // TaprootCommitScriptToSelf creates the taproot witness program that commits // to the revocation (keyspend) and delay path (script path) in a single // taproot output key. @@ -1752,35 +1838,71 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) func TaprootCommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { - // First, we'll need to construct the tapLeaf that'll be our delay CSV - // clause. - // - // TODO(roasbeef): extract into diff func - builder := txscript.NewScriptBuilder() - builder.AddData(schnorr.SerializePubKey(selfKey)) - builder.AddOp(txscript.OP_CHECKSIG) - builder.AddInt64(int64(csvTimeout)) - builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) - builder.AddOp(txscript.OP_DROP) + commitScriptTree, err := NewLocalCommitScriptTree( + csvTimeout, selfKey, revokeKey, + ) + if err != nil { + return nil, err + } - delayScript, err := builder.Script() + return commitScriptTree.TaprootKey, nil +} + +// TaprootCommitSpendSuccess constructs a valid witness allowing a node to +// sweep the settled taproot output after the delay has passed for a force +// close. +func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx, revokeKey *btcec.PublicKey, + scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // First, we'll need to construct a valid control block to execute the + // leaf script for sweep settlement. + settleTapleafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + settleIdx := scriptTree.LeafProofIndex[settleTapleafHash] + settleMerkleProof := scriptTree.LeafMerkleProofs[settleIdx] + settleControlBlock := settleMerkleProof.ToControlBlock(revokeKey) + + // With the control block created, we'll now generate the signature we + // need to authorize the spend. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) if err != nil { return nil, err } - // With the delay script computed, we'll now create a tapscript tree - // with a single leaf, and then obtain a root from that. - tapLeaf := txscript.NewBaseTapLeaf(delayScript) - tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) - tapScriptRoot := tapScriptTree.RootNode.TapHash() + // The final witness stack will be: + // + // + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = settleControlBlock.ToBytes() + if err != nil { + return nil, err + } - // Now that we have our root, we can arrive at the final output script - // by tweaking the internal key with this root. - toLocalOutputKey := txscript.ComputeTaprootOutputKey( - revokeKey, tapScriptRoot[:], - ) + return witnessStack, nil +} + +// TaprootCommitSpendRevoke constructs a valid witness allowing a node to sweep +// the revoked taproot output of a malicious peer. +func TaprootCommitSpendRevoke(signer Signer, signDesc *SignDescriptor, + revokeTx *wire.MsgTx) (wire.TxWitness, error) { - return toLocalOutputKey, nil + // For this spend type, we only need a single signature which'll be a + // keyspend using the revoke private key. + sweepSig, err := signer.SignOutputRaw(revokeTx, signDesc) + if err != nil { + return nil, err + } + + // The witness stack in this case is pretty simple: we only need to + // specify the signature generated. + witnessStack := make(wire.TxWitness, 1) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + + return witnessStack, nil } // LeaseCommitScriptToSelf constructs the public key script for the output on the @@ -1991,6 +2113,43 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } +// NewRemoteCommitScriptTree constructs a new script tree for the remote party +// to sweep their funds after a hard coded 1 block delay. +func NewRemoteCommitScriptTree(numsKey, + remoteKey *btcec.PublicKey) (*CommitScriptTree, error) { + + // First, construct the remote party's tapscript they'll use to sweep their + // outputs. + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(remoteKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + delayScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With this script constructed, we'll map that into a tapLeaf, then + // make a new tapscript root from that. + tapLeaf := txscript.NewBaseTapLeaf(delayScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptRoot := tapScriptTree.RootNode.TapHash() + + // Now that we have our root, we can arrive at the final output script + // by tweaking the internal key with this root. + toRemoteOutputKey := txscript.ComputeTaprootOutputKey( + numsKey, tapScriptRoot[:], + ) + + return &CommitScriptTree{ + TaprootKey: toRemoteOutputKey, + SettleLeaf: tapLeaf, + TapscriptTree: tapScriptTree, + TapscriptRoot: tapScriptRoot[:], + }, nil +} + // TaprootCommitScriptToRemote constructs a taproot witness program for the // output on the commitment transaction for the remote party. For the top level // key spend, we'll use the combined funding key (musig2.KeyAgg(k1, k2)), as a @@ -2012,31 +2171,50 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { func TaprootCommitScriptToRemote(combinedFundingKey, remoteKey *btcec.PublicKey) (*btcec.PublicKey, error) { - // First, construct the remote party's tapscript they'll use to sweep their - // outputs. - builder := txscript.NewScriptBuilder() - builder.AddData(schnorr.SerializePubKey(remoteKey)) - builder.AddOp(txscript.OP_CHECKSIG) - builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) - - delayScript, err := builder.Script() + commitScriptTree, err := NewRemoteCommitScriptTree( + combinedFundingKey, remoteKey, + ) if err != nil { return nil, err } - // With this script constructed, we'll map that into a tapLeaf, then - // make a new tapscript root from that. - tapLeaf := txscript.NewBaseTapLeaf(delayScript) - tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) - tapScriptRoot := tapScriptTree.RootNode.TapHash() + return commitScriptTree.TaprootKey, nil +} - // Now that we have our root, we can arrive at the final output script - // by tweaking the internal key with this root. - toRemoteOutputKey := txscript.ComputeTaprootOutputKey( - combinedFundingKey, tapScriptRoot[:], - ) +// TaprootCommitRemoteSpend allows the remote party to sweep their output into +// their wallet after an enforced 1 block delay. +func TaprootCommitRemoteSpend(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx, numsKey *btcec.PublicKey, + scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // First, we'll need to construct a valid control block to execute the + // leaf script for sweep settlement. + settleTapleafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + settleIdx := scriptTree.LeafProofIndex[settleTapleafHash] + settleMerkleProof := scriptTree.LeafMerkleProofs[settleIdx] + settleControlBlock := settleMerkleProof.ToControlBlock(numsKey) - return toRemoteOutputKey, nil + // With the control block created, we'll now generate the signature we + // need to authorize the spend. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // The final witness stack will be: + // + // + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = settleControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil } // LeaseCommitScriptToRemoteConfirmed constructs the script for the output on @@ -2135,15 +2313,29 @@ func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } -// TaprootOutputKeyAnchor returns the segwit v1 (taproot) witness program that -// encodes the anchor output spending conditions: the passed key can be used -// for keyspend, with the OP_CSV 16 clause living within an internal tapscript -// leaf. +// AnchorScriptTree holds all the contents needed to to sweep a taproot anchor +// output on chain. // -// Spend paths: -// - Key spend: -// - Script spend: OP_16 CSV -func TaprootOutputKeyAnchor(key *btcec.PublicKey) (*btcec.PublicKey, error) { +// TODO(roasbeef): refactor trees to reduce dedup +type AnchorScriptTree struct { + // TaprootKey is the key that will be used to generate the taproot + // output. + TaprootKey *btcec.PublicKey + + // SweepLeaf is the leaf used to settle the output after the delay. + SweepLeaf txscript.TapLeaf + + // TapscriptTree is the full tapscript tree that also includes the + // control block needed to spend each of the leaves. + TapscriptTree *txscript.IndexedTapScriptTree + + // TapscriptTreeRoot is the root hash of the tapscript tree. + TapscriptRoot []byte +} + +// NewAnchorScriptTree makes a new script tree for an anchor output with the +// passed anchor key. +func NewAnchorScriptTree(anchorKey *btcec.PublicKey) (*AnchorScriptTree, error) { // The main script used is just a OP_16 CSV (anyone can sweep after 16 // blocks). builder := txscript.NewScriptBuilder() @@ -2163,11 +2355,83 @@ func TaprootOutputKeyAnchor(key *btcec.PublicKey) (*btcec.PublicKey, error) { // Now that we have our root, we can arrive at the final output script // by tweaking the internal key with this root. - anchorKey := txscript.ComputeTaprootOutputKey( - key, tapScriptRoot[:], + anchorOutputKey := txscript.ComputeTaprootOutputKey( + anchorKey, tapScriptRoot[:], ) - return anchorKey, nil + return &AnchorScriptTree{ + TaprootKey: anchorOutputKey, + SweepLeaf: tapLeaf, + TapscriptTree: tapScriptTree, + TapscriptRoot: tapScriptRoot[:], + }, nil +} + +// TaprootOutputKeyAnchor returns the segwit v1 (taproot) witness program that +// encodes the anchor output spending conditions: the passed key can be used +// for keyspend, with the OP_CSV 16 clause living within an internal tapscript +// leaf. +// +// Spend paths: +// - Key spend: +// - Script spend: OP_16 CSV +func TaprootOutputKeyAnchor(key *btcec.PublicKey) (*btcec.PublicKey, error) { + anchorScriptTree, err := NewAnchorScriptTree(key) + if err != nil { + return nil, err + } + + return anchorScriptTree.TaprootKey, nil +} + +// TaprootAnchorSpend constructs a valid witness allowing a node to sweep their +// anchor output. +func TaprootAnchorSpend(signer Signer, signDesc *SignDescriptor, + revokeTx *wire.MsgTx) (wire.TxWitness, error) { + + // For this spend type, we only need a single signature which'll be a + // keyspend using the revoke private key. + sweepSig, err := signer.SignOutputRaw(revokeTx, signDesc) + if err != nil { + return nil, err + } + + // The witness stack in this case is pretty simple: we only need to + // specify the signature generated. + witnessStack := make(wire.TxWitness, 1) + witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + + return witnessStack, nil +} + +// TaprootAnchorSpendAny constructs a valid witness allowing anyone to sweep +// the anchor output after 16 blocks. +func TaprootAnchorSpendAny(anchorKey *btcec.PublicKey) (wire.TxWitness, error) { + anchorScriptTree, err := NewAnchorScriptTree(anchorKey) + if err != nil { + return nil, err + } + + // For this spend, the only thing we need to do is create a valid + // control block. Other than that, there're no restrictions to how the + // output can be spent. + scriptTree := anchorScriptTree.TapscriptTree + sweepLeaf := anchorScriptTree.SweepLeaf + sweepIdx := scriptTree.LeafProofIndex[sweepLeaf.TapHash()] + sweepMerkleProof := scriptTree.LeafMerkleProofs[sweepIdx] + sweepControlBlock := sweepMerkleProof.ToControlBlock(anchorKey) + + // The final witness stack will be: + // + // + witnessStack := make(wire.TxWitness, 2) + witnessStack[0] = sweepLeaf.Script + witnessStack[1], err = sweepControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil } // CommitSpendAnchor constructs a valid witness allowing a node to spend their diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 41f004a4b3c..1ad8d413120 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -52,7 +52,7 @@ func assertEngineExecution(t *testing.T, testNum int, valid bool, if err != nil { t.Fatalf("stepping (%v)\n", err) } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) + debugBuf.WriteString(fmt.Sprintf("Stepping %v\n", dis)) done, err = vm.Step() if err != nil && valid { @@ -65,8 +65,11 @@ func assertEngineExecution(t *testing.T, testNum int, valid bool, "should be invalid: %v", testNum, err) } - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) + debugBuf.WriteString(fmt.Sprintf("Stack: %v\n", + vm.GetStack())) + debugBuf.WriteString(fmt.Sprintf("AltStack: %v\n", + vm.GetAltStack())) + debugBuf.WriteString("-----\n") } // If we get to this point the unexpected case was not reached diff --git a/input/taproot_test.go b/input/taproot_test.go new file mode 100644 index 00000000000..74ec1db034a --- /dev/null +++ b/input/taproot_test.go @@ -0,0 +1,1877 @@ +package input + +import ( + "crypto/rand" + "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/keychain" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +type testSenderHtlcScriptTree struct { + preImage lntypes.Preimage + + senderKey *btcec.PrivateKey + + receiverKey *btcec.PrivateKey + + revokeKey *btcec.PrivateKey + + htlcTxOut *wire.TxOut + + *HtlcScriptTree + + rootHash []byte + + htlcAmt int64 + + lockTime int32 +} + +func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { + var preImage lntypes.Preimage + _, err := rand.Read(preImage[:]) + require.NoError(t, err) + + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + payHash := preImage.Hash() + htlcScriptTree, err := SenderHTLCScriptTaproot( + senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), + payHash[:], + ) + require.NoError(t, err) + + const htlcAmt = 100 + pkScript, err := PayToTaprootScript(htlcScriptTree.TaprootKey) + require.NoError(t, err) + + targetTxOut := &wire.TxOut{ + Value: htlcAmt, + PkScript: pkScript, + } + + return &testSenderHtlcScriptTree{ + preImage: preImage, + senderKey: senderKey, + receiverKey: receiverKey, + revokeKey: revokeKey, + htlcTxOut: targetTxOut, + htlcAmt: htlcAmt, + rootHash: htlcScriptTree.TapscriptRoot, + HtlcScriptTree: htlcScriptTree, + } +} + +type witnessGen func(*wire.MsgTx, *txscript.TxSigHashes, + txscript.PrevOutputFetcher) (wire.TxWitness, error) + +func htlcSenderRedeemValidWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testSenderHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + receiverKey := htlcScriptTree.receiverKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + receiverKey, + }, + } + + successLeaf := htlcScriptTree.SuccessTapLeaf + scriptTree := htlcScriptTree.HtlcScriptTree + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: receiverKey.PubKey(), + }, + WitnessScript: successLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return SenderHTLCScriptTaprootRedeem( + signer, signDesc, spendTx, + htlcScriptTree.preImage[:], + htlcScriptTree.revokeKey.PubKey(), + scriptTree.TapscriptTree, + ) + } +} + +func htlcSenderRevocationWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testSenderHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + revokeKey := htlcScriptTree.revokeKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + revokeKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootKeySpendSignMethod, + TapTweak: htlcScriptTree.TapscriptRoot, + PrevOutputFetcher: prevOuts, + } + + return SenderHTLCScriptTaprootRevoke( + signer, signDesc, spendTx, + ) + } +} + +func htlcSenderTimeoutWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testSenderHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + timeoutLeaf := htlcScriptTree.TimeoutTapLeaf + scriptTree := htlcScriptTree.HtlcScriptTree + + receiverSigner := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + htlcScriptTree.receiverKey, + }, + } + receiverDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: htlcScriptTree.receiverKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + receiverSig, err := receiverSigner.SignOutputRaw( + spendTx, receiverDesc, + ) + if err != nil { + return nil, err + } + + senderKey := htlcScriptTree.senderKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + senderKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: senderKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return SenderHTLCScriptTaprootTimeout( + receiverSig, sigHash, signer, signDesc, spendTx, + htlcScriptTree.revokeKey.PubKey(), + scriptTree.TapscriptTree, + ) + } +} + +// 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() + + // First, create a new test script tree. + htlcScriptTree := newTestSenderHtlcScriptTree(t) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: htlcScriptTree.htlcAmt, + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + valid bool + }{ + // Valid redeem with the pre-image, and the spending + // transaction set to CSV 1 to enforce the required delay. + { + name: "redeem success valid sighash all", + witnessGen: htlcSenderRedeemValidWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Valid with pre-image, using sighash default. + { + name: "redeem success valid sighash default", + witnessGen: htlcSenderRedeemValidWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Valid with pre-image, using sighash single+anyonecanpay. + { + name: "redeem success valid sighash " + + "single|anyonecanpay", + witnessGen: htlcSenderRedeemValidWitnessGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Invalid spend, the witness is correct, but the spending tx + // doesn't have a sequence of 1 set. This uses the CSV 0 trick: + // 0 > 0 -> false. + { + name: "redeem success invalid wrong sequence", + witnessGen: htlcSenderRedeemValidWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: false, + }, + + // Valid spend with the revocation key, sighash all. + { + name: "revocation spend vaild sighash all", + witnessGen: htlcSenderRevocationWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + // Valid spend with the revocation key, sighash default. + { + name: "revocation spend vaild sighash default", + witnessGen: htlcSenderRevocationWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + valid: true, + }, + + // Valid spend with the revocation key, sighash single+anyone + // can pay. + { + name: "revocation spend vaild sighash " + + "single|anyonecanpay", + witnessGen: htlcSenderRevocationWitnessGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + valid: true, + }, + + // Invalid spend with the revocation key. The witness mutator + // modifies the sig. + { + name: "revocation spend invalid", + witnessGen: htlcSenderRevocationWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + // Valid spend of the timeout path, sighash default. + { + name: "timeout spend valid", + witnessGen: htlcSenderTimeoutWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + valid: true, + }, + + // Valid spend of the timeout path, sighash all. + { + name: "timeout spend valid sighash all", + witnessGen: htlcSenderTimeoutWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + // Valid spend of the timeout path, sighash single. + { + name: "timeout spend valid sighash single", + witnessGen: htlcSenderTimeoutWitnessGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + valid: true, + }, + + // Invalid spend of timeout path, invalid reciever sig. + { + name: "timeout spend invalid receiver sig", + witnessGen: htlcSenderTimeoutWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + // Invalid spend of timeout path, invalid sender sig. + { + name: "timeout spend invalid sender sig", + witnessGen: htlcSenderTimeoutWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[1][0] ^= 1 + }, + valid: false, + }, + } + + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.htlcTxOut.PkScript, + htlcScriptTree.htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + htlcScriptTree.htlcTxOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, htlcScriptTree.htlcAmt, + txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.htlcTxOut.PkScript, + htlcScriptTree.htlcAmt, + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} + +type testReceiverHtlcScriptTree struct { + preImage lntypes.Preimage + + senderKey *btcec.PrivateKey + + receiverKey *btcec.PrivateKey + + revokeKey *btcec.PrivateKey + + htlcTxOut *wire.TxOut + + *HtlcScriptTree + + rootHash []byte + + htlcAmt int64 + + lockTime int32 +} + +func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { + var preImage lntypes.Preimage + _, err := rand.Read(preImage[:]) + require.NoError(t, err) + + senderKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + revokeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + const cltvExpiry = 144 + + payHash := preImage.Hash() + htlcScriptTree, err := ReceiverHTLCScriptTaproot( + cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(), + revokeKey.PubKey(), payHash[:], + ) + require.NoError(t, err) + + const htlcAmt = 100 + pkScript, err := PayToTaprootScript(htlcScriptTree.TaprootKey) + require.NoError(t, err) + + targetTxOut := &wire.TxOut{ + Value: htlcAmt, + PkScript: pkScript, + } + + return &testReceiverHtlcScriptTree{ + preImage: preImage, + senderKey: senderKey, + receiverKey: receiverKey, + revokeKey: revokeKey, + htlcTxOut: targetTxOut, + htlcAmt: htlcAmt, + rootHash: htlcScriptTree.TapscriptRoot, + lockTime: cltvExpiry, + HtlcScriptTree: htlcScriptTree, + } +} + +func htlcReceiverTimeoutWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testReceiverHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + senderKey := htlcScriptTree.senderKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + senderKey, + }, + } + + timeoutLeaf := htlcScriptTree.TimeoutTapLeaf + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: senderKey.PubKey(), + }, + WitnessScript: timeoutLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + // With the lock time in place, we can now generate the timeout + // witness. + return ReceiverHTLCScriptTaprootTimeout( + signer, signDesc, spendTx, htlcScriptTree.lockTime, + htlcScriptTree.revokeKey.PubKey(), + htlcScriptTree.TapscriptTree, + ) + } +} + +func htlcReceiverRevocationWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testReceiverHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + revokeKey := htlcScriptTree.revokeKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + revokeKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootKeySpendSignMethod, + TapTweak: htlcScriptTree.TapscriptRoot, + PrevOutputFetcher: prevOuts, + } + + return ReceiverHTLCScriptTaprootRevoke( + signer, signDesc, spendTx, + ) + } +} + +func htlcReceiverSuccessWitnessGen(sigHash txscript.SigHashType, + htlcScriptTree *testReceiverHtlcScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + successsLeaf := htlcScriptTree.SuccessTapLeaf + scriptTree := htlcScriptTree.HtlcScriptTree + + senderSigner := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + htlcScriptTree.senderKey, + }, + } + senderDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: htlcScriptTree.senderKey.PubKey(), + }, + WitnessScript: successsLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + senderSig, err := senderSigner.SignOutputRaw( + spendTx, senderDesc, + ) + if err != nil { + return nil, err + } + + receiverKey := htlcScriptTree.receiverKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + receiverKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: receiverKey.PubKey(), + }, + WitnessScript: successsLeaf.Script, + Output: htlcScriptTree.htlcTxOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return ReceiverHTLCScriptTaprootRedeem( + senderSig, sigHash, htlcScriptTree.preImage[:], + signer, signDesc, spendTx, + htlcScriptTree.revokeKey.PubKey(), + scriptTree.TapscriptTree, + ) + } +} + +// 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() + + // 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) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: htlcScriptTree.htlcAmt, + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + txMutator func(tx *wire.MsgTx) + + valid bool + }{ + // Valid timeout by the sender after the timeout period has + // passed. We also use a sequence of 1 as the sender must wait + // a single block before being able to sweep the HTLC. + { + name: "timeout valid sig hash all", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Valid timeout like above, but sighash default. + { + name: "timeout valid sig hash default", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Valid timeout like above, but sighash single. + { + name: "timeout valid sig hash single", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + // Invalid timeout case, the sequence of the spending + // transaction isn't set to 1. + { + name: "timeout invalid wrong sequence", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: false, + }, + + // Invalid timeout case, the lock time is set to the wrong + // value. + { + name: "timeout invalid wrong lock time", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + txMutator: func(tx *wire.MsgTx) { + tx.LockTime = 0 + }, + valid: false, + }, + + // Invalid timeout case, the signature is invalid. + { + name: "timeout invalid wrong sig", + witnessGen: htlcReceiverTimeoutWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + // Valid spend of the revocation path. + { + name: "revocation spend valid", + witnessGen: htlcReceiverRevocationWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + // Invalid spend of the revocation path. + { + name: "revocation spend valid", + witnessGen: htlcReceiverRevocationWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + // Valid success spend w/ pre-image and sender sig. + { + name: "success spend valid", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + // Valid succcess spend sighash default. + { + name: "success spend valid sighash default", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + // Valid succcess spend sighash default. + { + name: "success spend valid sig hash default", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashDefault, htlcScriptTree, + ), + valid: true, + }, + + // Valid succcess spend sighash single. + { + name: "success spend valid sighash single", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + valid: true, + }, + + // Invalid success spend, wrong pre-image. + { + name: "success spend invalid preimage", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + // The pre-image is the 3rd item (starting from + // the "bottom") of the witness stack). + wit[2][0] ^= 1 + }, + valid: false, + }, + + // Invalid success spend, invalid sender sig. + { + name: "success spend invalid sender sig", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + // Flip a bit in the sender sig which is the + // first element of the witness stack. + wit[0][0] ^= 1 + }, + valid: false, + }, + + // Invalid success spend, invalid receiver sig. + { + name: "success spend invalid receiver sig", + witnessGen: htlcReceiverSuccessWitnessGen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + // Flip a bit in the receiver sig which is the + // second element of the witness stack. + wit[1][0] ^= 1 + }, + valid: false, + }, + } + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + // TODO(roasbeef): consolidate w/ above + + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.htlcTxOut.PkScript, + htlcScriptTree.htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.txMutator != nil { + testCase.txMutator(spendTxCopy) + } + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + htlcScriptTree.htlcTxOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, htlcScriptTree.htlcAmt, + txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.htlcTxOut.PkScript, + htlcScriptTree.htlcAmt, + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} + +type testCommitScriptTree struct { + csvDelay uint32 + + selfKey *btcec.PrivateKey + + revokeKey *btcec.PrivateKey + + selfAmt btcutil.Amount + + txOut *wire.TxOut + + *CommitScriptTree +} + +func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { + selfKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + revokeKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + const ( + csvDelay = 6 + selfAmt = 1000 + ) + + var commitScriptTree *CommitScriptTree + if local { + commitScriptTree, err = NewLocalCommitScriptTree( + csvDelay, selfKey.PubKey(), revokeKey.PubKey(), + ) + } else { + commitScriptTree, err = NewRemoteCommitScriptTree( + revokeKey.PubKey(), selfKey.PubKey(), + ) + } + + pkScript, err := PayToTaprootScript(commitScriptTree.TaprootKey) + if err != nil { + return nil, err + } + + return &testCommitScriptTree{ + csvDelay: csvDelay, + selfKey: selfKey, + revokeKey: revokeKey, + selfAmt: selfAmt, + txOut: &wire.TxOut{ + PkScript: pkScript, + Value: selfAmt, + }, + CommitScriptTree: commitScriptTree, + }, nil +} + +func localCommitSweepWitGen(sigHash txscript.SigHashType, + commitScriptTree *testCommitScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + selfKey := commitScriptTree.selfKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + selfKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: selfKey.PubKey(), + }, + WitnessScript: commitScriptTree.SettleLeaf.Script, + Output: commitScriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return TaprootCommitSpendSuccess( + signer, signDesc, spendTx, + commitScriptTree.revokeKey.PubKey(), + commitScriptTree.TapscriptTree, + ) + } +} + +func localCommitRevokeWitGen(sigHash txscript.SigHashType, + commitScriptTree *testCommitScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + revokeKey := commitScriptTree.revokeKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + revokeKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + Output: commitScriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootKeySpendSignMethod, + TapTweak: commitScriptTree.TapscriptRoot, + PrevOutputFetcher: prevOuts, + } + + return TaprootCommitSpendRevoke( + signer, signDesc, spendTx, + ) + } +} + +// 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() + + commitScriptTree, err := newTestCommitScriptTree(true) + require.NoError(t, err) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: int64(commitScriptTree.selfAmt), + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + valid bool + }{ + { + name: "valid sweep to self", + witnessGen: localCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = commitScriptTree.csvDelay + }, + valid: true, + }, + + { + name: "valid sweep to self sighash default", + witnessGen: localCommitSweepWitGen( + txscript.SigHashDefault, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = commitScriptTree.csvDelay + }, + valid: true, + }, + + { + name: "valid sweep to self sighash single", + witnessGen: localCommitSweepWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = commitScriptTree.csvDelay + }, + valid: true, + }, + + { + name: "invalid sweep to self wrong sequence", + witnessGen: localCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: false, + }, + + { + name: "invalid sweep to self bad sig", + witnessGen: localCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + { + name: "valid revocation sweep", + witnessGen: localCommitRevokeWitGen( + txscript.SigHashAll, commitScriptTree, + ), + valid: true, + }, + + { + name: "valid revocation sweep sighash default", + witnessGen: localCommitRevokeWitGen( + txscript.SigHashDefault, commitScriptTree, + ), + valid: true, + }, + + { + name: "valid revocation sweep sighash single", + witnessGen: localCommitRevokeWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + commitScriptTree, + ), + valid: true, + }, + + { + name: "invalid revocation sweep bad sig", + witnessGen: localCommitRevokeWitGen( + txscript.SigHashAll, commitScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + } + + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + commitScriptTree.txOut.PkScript, + int64(commitScriptTree.selfAmt), + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + commitScriptTree.txOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, int64(commitScriptTree.selfAmt), + txscript.NewCannedPrevOutputFetcher( + commitScriptTree.txOut.PkScript, + int64(commitScriptTree.selfAmt), + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} + +func remoteCommitSweepWitGen(sigHash txscript.SigHashType, + commitScriptTree *testCommitScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + selfKey := commitScriptTree.selfKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + selfKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: selfKey.PubKey(), + }, + WitnessScript: commitScriptTree.SettleLeaf.Script, + Output: commitScriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return TaprootCommitRemoteSpend( + signer, signDesc, spendTx, + commitScriptTree.revokeKey.PubKey(), + commitScriptTree.TapscriptTree, + ) + } +} + +// TestTaprootCommitScriptRemote tests that the remote party can properly sweep +// their output after force close. +func TestTaprootCommitScriptRemote(t *testing.T) { + t.Parallel() + + commitScriptTree, err := newTestCommitScriptTree(false) + require.NoError(t, err) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: int64(commitScriptTree.selfAmt), + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + valid bool + }{ + { + name: "valid remote sweep", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + { + name: "valid remote sweep sighash default", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashDefault, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + { + name: "valid remote sweep sighash single", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + valid: true, + }, + + { + name: "invalid remote sweep wrong sequence", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 0 + }, + valid: false, + }, + + { + name: "invalid bad sig", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + + { + name: "invalid bad sig right sequence", + witnessGen: remoteCommitSweepWitGen( + txscript.SigHashAll, commitScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + witnessMutator: func(wit wire.TxWitness) { + wit[0][0] ^= 1 + }, + valid: false, + }, + } + + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + commitScriptTree.txOut.PkScript, + int64(commitScriptTree.selfAmt), + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + commitScriptTree.txOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, int64(commitScriptTree.selfAmt), + txscript.NewCannedPrevOutputFetcher( + commitScriptTree.txOut.PkScript, + int64(commitScriptTree.selfAmt), + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} + +type testAnchorScriptTree struct { + sweepKey *btcec.PrivateKey + + amt btcutil.Amount + + txOut *wire.TxOut + + *AnchorScriptTree +} + +func newTestAnchorScripTree() (*testAnchorScriptTree, error) { + sweepKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + anchorScriptTree, err := NewAnchorScriptTree(sweepKey.PubKey()) + if err != nil { + return nil, err + } + + const amt = 1_000 + + pkScript, err := PayToTaprootScript(anchorScriptTree.TaprootKey) + if err != nil { + return nil, err + } + + return &testAnchorScriptTree{ + sweepKey: sweepKey, + amt: amt, + txOut: &wire.TxOut{ + PkScript: pkScript, + Value: amt, + }, + AnchorScriptTree: anchorScriptTree, + }, nil +} + +func anchorSweepWitGen(sigHash txscript.SigHashType, + anchorScriptTree *testAnchorScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + sweepKey := anchorScriptTree.sweepKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + sweepKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: sweepKey.PubKey(), + }, + Output: anchorScriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootKeySpendSignMethod, + TapTweak: anchorScriptTree.TapscriptRoot, + PrevOutputFetcher: prevOuts, + } + + return TaprootAnchorSpend( + signer, signDesc, spendTx, + ) + } +} + +func anchorAnySweepWitGen(sigHash txscript.SigHashType, + anchorScriptTree *testAnchorScriptTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + return TaprootAnchorSpendAny( + anchorScriptTree.sweepKey.PubKey(), + ) + } +} + +// TestTaprootCommitScript tests that a channel peer can properly spend the +// anchor, and that anyone can spend it after 16 blocks. +func TestTaprootAnchorScript(t *testing.T) { + t.Parallel() + + anchorScriptTree, err := newTestAnchorScripTree() + require.NoError(t, err) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: int64(anchorScriptTree.amt), + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + valid bool + }{ + { + name: "valid anchor sweep", + witnessGen: anchorSweepWitGen( + txscript.SigHashAll, anchorScriptTree, + ), + valid: true, + }, + + { + name: "valid anchor sweep sighash default", + witnessGen: anchorSweepWitGen( + txscript.SigHashDefault, anchorScriptTree, + ), + valid: true, + }, + + { + name: "valid anchor sweep single", + witnessGen: anchorSweepWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + anchorScriptTree, + ), + valid: true, + }, + + { + name: "invalid anchor sweep bad sig", + witnessGen: anchorSweepWitGen( + txscript.SigHashAll, anchorScriptTree, + ), + witnessMutator: func(witness wire.TxWitness) { + witness[0][0] ^= 1 + }, + valid: false, + }, + + { + name: "valid 3rd party sweep", + witnessGen: anchorAnySweepWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + anchorScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 16 + }, + valid: true, + }, + + { + name: "invalid 3rd party sweep", + witnessGen: anchorAnySweepWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + anchorScriptTree, + ), + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 2 + }, + valid: false, + }, + } + + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + anchorScriptTree.txOut.PkScript, + int64(anchorScriptTree.amt), + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + anchorScriptTree.txOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, int64(anchorScriptTree.amt), + txscript.NewCannedPrevOutputFetcher( + anchorScriptTree.txOut.PkScript, + int64(anchorScriptTree.amt), + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} + +type testSecondLevelHtlcTree struct { + delayKey *btcec.PrivateKey + + revokeKey *btcec.PrivateKey + + csvDelay uint32 + + amt btcutil.Amount + + txOut *wire.TxOut + + scriptTree *txscript.IndexedTapScriptTree + + tapScriptRoot []byte +} + +func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { + delayKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + revokeKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + const csvDelay = 6 + + scriptTree, err := SecondLevelHtlcTapscriptTree( + delayKey.PubKey(), csvDelay, + ) + if err != nil { + return nil, err + } + + tapScriptRoot := scriptTree.RootNode.TapHash() + + htlcKey := txscript.ComputeTaprootOutputKey( + revokeKey.PubKey(), tapScriptRoot[:], + ) + + pkScript, err := PayToTaprootScript(htlcKey) + if err != nil { + return nil, err + } + + const amt = 100 + + return &testSecondLevelHtlcTree{ + delayKey: delayKey, + revokeKey: revokeKey, + csvDelay: csvDelay, + txOut: &wire.TxOut{ + PkScript: pkScript, + Value: amt, + }, + amt: amt, + scriptTree: scriptTree, + tapScriptRoot: tapScriptRoot[:], + }, nil +} + +func secondLevelHtlcSuccessWitGen(sigHash txscript.SigHashType, + scriptTree *testSecondLevelHtlcTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + selfKey := scriptTree.delayKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + selfKey, + }, + } + + tapLeaf := scriptTree.scriptTree.LeafMerkleProofs[0].TapLeaf + witnessScript := tapLeaf.Script + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: selfKey.PubKey(), + }, + WitnessScript: witnessScript, + Output: scriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return TaprootHtlcSpendSuccess( + signer, signDesc, scriptTree.revokeKey.PubKey(), + spendTx, scriptTree.scriptTree, + ) + } +} + +func secondLevelHtlcRevokeWitnessgen(sigHash txscript.SigHashType, + scriptTree *testSecondLevelHtlcTree) witnessGen { + + return func(spendTx *wire.MsgTx, hashCache *txscript.TxSigHashes, + prevOuts txscript.PrevOutputFetcher) (wire.TxWitness, error) { + + revokeKey := scriptTree.revokeKey + signer := &MockSigner{ + Privkeys: []*btcec.PrivateKey{ + revokeKey, + }, + } + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + Output: scriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootKeySpendSignMethod, + TapTweak: scriptTree.tapScriptRoot, + PrevOutputFetcher: prevOuts, + } + + return TaprootHtlcSpendRevoke( + signer, signDesc, spendTx, + ) + } +} + +// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly +// spend the second level HTLC script to resolve HTLCs. +func TestTaprootSecondLevelHtlcScript(t *testing.T) { + t.Parallel() + + htlcScriptTree, err := newTestSecondLevelHtlcTree() + require.NoError(t, err) + + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{}) + spendTx.AddTxOut(&wire.TxOut{ + Value: int64(htlcScriptTree.amt), + }) + + testCases := []struct { + name string + + witnessGen witnessGen + + txInMutator func(txIn *wire.TxIn) + + witnessMutator func(witness wire.TxWitness) + + valid bool + }{ + { + name: "valid success sweep", + witnessGen: secondLevelHtlcSuccessWitGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = htlcScriptTree.csvDelay + }, + }, + + { + name: "valid success sweep sighash default", + witnessGen: secondLevelHtlcSuccessWitGen( + txscript.SigHashDefault, htlcScriptTree, + ), + valid: true, + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = htlcScriptTree.csvDelay + }, + }, + + { + name: "valid success sweep sighash single", + witnessGen: secondLevelHtlcSuccessWitGen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + valid: true, + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = htlcScriptTree.csvDelay + }, + }, + + { + name: "invalid success sweep bad sig", + witnessGen: secondLevelHtlcSuccessWitGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: false, + witnessMutator: func(witness wire.TxWitness) { + witness[0][0] ^= 0x01 + }, + }, + + { + name: "invalid success sweep bad sequence", + witnessGen: secondLevelHtlcSuccessWitGen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: false, + txInMutator: func(txIn *wire.TxIn) { + txIn.Sequence = 1 + }, + }, + + { + name: "valid revocation sweep", + witnessGen: secondLevelHtlcRevokeWitnessgen( + txscript.SigHashAll, htlcScriptTree, + ), + valid: true, + }, + + { + name: "valid revocation sweep sig hash deafult", + witnessGen: secondLevelHtlcRevokeWitnessgen( + txscript.SigHashDefault, htlcScriptTree, + ), + valid: true, + }, + + { + name: "valid revocation sweep single", + witnessGen: secondLevelHtlcRevokeWitnessgen( + txscript.SigHashSingle| + txscript.SigHashAnyOneCanPay, + htlcScriptTree, + ), + valid: true, + }, + + { + name: "invalid revocation sweep", + witnessGen: secondLevelHtlcRevokeWitnessgen( + txscript.SigHashAll, htlcScriptTree, + ), + witnessMutator: func(witness wire.TxWitness) { + witness[0][0] ^= 0x01 + }, + valid: false, + }, + } + + for i, testCase := range testCases { + testCase := testCase + spendTxCopy := spendTx.Copy() + + t.Run(testCase.name, func(t *testing.T) { + if testCase.txInMutator != nil { + testCase.txInMutator(spendTxCopy.TxIn[0]) + } + + prevOuts := txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.txOut.PkScript, + int64(htlcScriptTree.amt), + ) + hashCache := txscript.NewTxSigHashes( + spendTxCopy, prevOuts, + ) + + var err error + spendTxCopy.TxIn[0].Witness, err = testCase.witnessGen( + spendTxCopy, hashCache, prevOuts, + ) + require.NoError(t, err) + + if testCase.witnessMutator != nil { + testCase.witnessMutator( + spendTxCopy.TxIn[0].Witness, + ) + } + + // With the witness generated, we'll now check for + // script validity. + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + htlcScriptTree.txOut.PkScript, + spendTxCopy, 0, txscript.StandardVerifyFlags, + nil, hashCache, int64(htlcScriptTree.amt), + txscript.NewCannedPrevOutputFetcher( + htlcScriptTree.txOut.PkScript, + int64(htlcScriptTree.amt), + ), + ) + } + assertEngineExecution(t, i, testCase.valid, newEngine) + }) + } +} diff --git a/input/test_utils.go b/input/test_utils.go index c7e26f9e73b..35f372c4341 100644 --- a/input/test_utils.go +++ b/input/test_utils.go @@ -72,9 +72,70 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx, return nil, fmt.Errorf("mock signer does not have key") } - sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, - signDesc.InputIndex, signDesc.Output.Value, signDesc.WitnessScript, - signDesc.HashType, privKey) + // In case of a taproot output any signature is always a Schnorr + // signature, based on the new tapscript sighash algorithm. + // + // TODO(roasbeef): should conslidate with btcwallet/signer.go + if txscript.IsPayToTaproot(signDesc.Output.PkScript) { + sigHashes := txscript.NewTxSigHashes( + tx, signDesc.PrevOutputFetcher, + ) + + witnessScript := signDesc.WitnessScript + + // Are we spending a script path or the key path? The API is + // slightly different, so we need to account for that to get the + // raw signature. + var ( + rawSig []byte + err error + ) + switch signDesc.SignMethod { + case TaprootKeySpendBIP0086SignMethod, + TaprootKeySpendSignMethod: + + // This function tweaks the private key using the tap + // root key supplied as the tweak. + rawSig, err = txscript.RawTxInTaprootSignature( + tx, sigHashes, signDesc.InputIndex, + signDesc.Output.Value, signDesc.Output.PkScript, + signDesc.TapTweak, signDesc.HashType, + privKey, + ) + if err != nil { + return nil, err + } + + case TaprootScriptSpendSignMethod: + leaf := txscript.TapLeaf{ + LeafVersion: txscript.BaseLeafVersion, + Script: witnessScript, + } + rawSig, err = txscript.RawTxInTapscriptSignature( + tx, sigHashes, signDesc.InputIndex, + signDesc.Output.Value, signDesc.Output.PkScript, + leaf, signDesc.HashType, privKey, + ) + if err != nil { + return nil, err + } + } + + sig, err := schnorr.ParseSignature( + rawSig[:schnorr.SignatureSize], + ) + if err != nil { + return nil, err + } + + return sig, nil + } + + sig, err := txscript.RawTxInWitnessSignature( + tx, signDesc.SigHashes, signDesc.InputIndex, + signDesc.Output.Value, signDesc.WitnessScript, + signDesc.HashType, privKey, + ) if err != nil { return nil, err } From 6aca853b49ad0dffc293e9065e2bb0cafddd2f1e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 23 May 2023 16:49:26 -0700 Subject: [PATCH 14/18] input: restore usage of NUMS key for to_remote output In this commit, we restore usage of the NUMS key for the to remote output, as this allows a remote party to scan the chain in order to find their remote output that in emergency recovery scenarios. --- input/script_utils.go | 62 ++++++++++++++++++++++++++++++++----------- input/taproot_test.go | 3 +-- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 8ced5d36368..adaff2d9341 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -3,6 +3,7 @@ package input import ( "bytes" "crypto/sha256" + "encoding/hex" "fmt" "github.com/btcsuite/btcd/btcec/v2" @@ -23,6 +24,34 @@ var ( SequenceLockTimeSeconds = uint32(1 << 22) ) +// mustParsePubKey parses a hex encoded public key string into a public key and +// panic if parsing fails. +func mustParsePubKey(pubStr string) btcec.PublicKey { + pubBytes, err := hex.DecodeString(pubStr) + if err != nil { + panic(err) + } + + pub, err := btcec.ParsePubKey(pubBytes) + if err != nil { + panic(err) + } + + return *pub +} + +// TaprootNUMSHex is the hex encoded version of the taproot NUMs key. +const TaprootNUMSHex = "02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb" + + "6bf4bc130a279" + +var ( + // TaprootNUMSKey is a NUMS key (nothing up my sleeves number) that has + // no known private key. This was generated using the following script: + // https://github.com/lightninglabs/lightning-node-connect/tree/master/mailbox/numsgen), + // with the seed phrase "Lightning Simple Taproot". + TaprootNUMSKey = mustParsePubKey(TaprootNUMSHex) +) + // Signature is an interface for objects that can populate signatures during // witness construction. type Signature interface { @@ -1115,7 +1144,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // With the two leaves obtained, we'll now make the tapscript tree, // then obtain the root from that tapscriptTree := txscript.AssembleTaprootScriptTree( - successTapLeaf, timeoutTapLeaf, + timeoutTapLeaf, successTapLeaf, ) tapScriptRoot := tapscriptTree.RootNode.TapHash() @@ -1802,7 +1831,7 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // Now that we have our root, we can arrive at the final output script // by tweaking the internal key with this root. toLocalOutputKey := txscript.ComputeTaprootOutputKey( - revokeKey, tapScriptRoot[:], + &TaprootNUMSKey, tapScriptRoot[:], ) return &CommitScriptTree{ @@ -1862,7 +1891,9 @@ func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor, ).TapHash() settleIdx := scriptTree.LeafProofIndex[settleTapleafHash] settleMerkleProof := scriptTree.LeafMerkleProofs[settleIdx] - settleControlBlock := settleMerkleProof.ToControlBlock(revokeKey) + settleControlBlock := settleMerkleProof.ToControlBlock( + &TaprootNUMSKey, + ) // With the control block created, we'll now generate the signature we // need to authorize the spend. @@ -2115,8 +2146,8 @@ 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(numsKey, - remoteKey *btcec.PublicKey) (*CommitScriptTree, error) { +func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, +) (*CommitScriptTree, error) { // First, construct the remote party's tapscript they'll use to sweep their // outputs. @@ -2139,7 +2170,7 @@ func NewRemoteCommitScriptTree(numsKey, // Now that we have our root, we can arrive at the final output script // by tweaking the internal key with this root. toRemoteOutputKey := txscript.ComputeTaprootOutputKey( - numsKey, tapScriptRoot[:], + &TaprootNUMSKey, tapScriptRoot[:], ) return &CommitScriptTree{ @@ -2152,9 +2183,10 @@ func NewRemoteCommitScriptTree(numsKey, // TaprootCommitScriptToRemote constructs a taproot witness program for the // output on the commitment transaction for the remote party. For the top level -// key spend, we'll use the combined funding key (musig2.KeyAgg(k1, k2)), as a -// sort of practical NUMs point (the local party would never sign for this). We -// then commit to a single tapscript leaf that holds the normal CSV 1 delay +// key spend, we'll use a NUMs key to ensure that only the script path can be +// taken. Using a set NUMs key here also means that recovery solutions can scan +// the chain given knowledge of the public key fo the remote party. We then +// commit to a single tapscript leaf that holds the normal CSV 1 delay // script. // // Our single tapleaf will use the following script: @@ -2168,12 +2200,10 @@ func NewRemoteCommitScriptTree(numsKey, // the stack. // // TODO(roasbeef): double check here can't pass additional stack elements? -func TaprootCommitScriptToRemote(combinedFundingKey, - remoteKey *btcec.PublicKey) (*btcec.PublicKey, error) { +func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, +) (*btcec.PublicKey, error) { - commitScriptTree, err := NewRemoteCommitScriptTree( - combinedFundingKey, remoteKey, - ) + commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey) if err != nil { return nil, err } @@ -2184,7 +2214,7 @@ func TaprootCommitScriptToRemote(combinedFundingKey, // TaprootCommitRemoteSpend allows the remote party to sweep their output into // their wallet after an enforced 1 block delay. func TaprootCommitRemoteSpend(signer Signer, signDesc *SignDescriptor, - sweepTx *wire.MsgTx, numsKey *btcec.PublicKey, + sweepTx *wire.MsgTx, scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { // First, we'll need to construct a valid control block to execute the @@ -2194,7 +2224,7 @@ func TaprootCommitRemoteSpend(signer Signer, signDesc *SignDescriptor, ).TapHash() settleIdx := scriptTree.LeafProofIndex[settleTapleafHash] settleMerkleProof := scriptTree.LeafMerkleProofs[settleIdx] - settleControlBlock := settleMerkleProof.ToControlBlock(numsKey) + settleControlBlock := settleMerkleProof.ToControlBlock(&TaprootNUMSKey) // With the control block created, we'll now generate the signature we // need to authorize the spend. diff --git a/input/taproot_test.go b/input/taproot_test.go index 74ec1db034a..2745a842d7e 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -927,7 +927,7 @@ func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { ) } else { commitScriptTree, err = NewRemoteCommitScriptTree( - revokeKey.PubKey(), selfKey.PubKey(), + selfKey.PubKey(), ) } @@ -1210,7 +1210,6 @@ func remoteCommitSweepWitGen(sigHash txscript.SigHashType, return TaprootCommitRemoteSpend( signer, signDesc, spendTx, - commitScriptTree.revokeKey.PubKey(), commitScriptTree.TapscriptTree, ) } From 37079b239babd53730491139a339d47fe24cfec7 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 23 May 2023 17:19:06 -0700 Subject: [PATCH 15/18] input: use script path for revocation clause for to_local output In this commit, we modify the to_local script to use a script path for the revocation scenario. With this change, we ensure that the internal key is always revealed which means the anchor outputs can still always be swept. --- input/script_utils.go | 95 ++++++++++++++++++++++++++++++++----------- input/taproot_test.go | 6 +-- 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index adaff2d9341..94e50c94e62 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -1793,6 +1793,10 @@ type CommitScriptTree struct { // SettleLeaf is the leaf used to settle the output after the delay. SettleLeaf txscript.TapLeaf + // RevocationLeaf is the leaf used to spend the output with the + // revocation key signature. + RevocationLeaf txscript.TapLeaf + // TapscriptTree is the full tapscript tree that also includes the // control block needed to spend each of the leaves. TapscriptTree *txscript.IndexedTapScriptTree @@ -1808,8 +1812,6 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // First, we'll need to construct the tapLeaf that'll be our delay CSV // clause. - // - // TODO(roasbeef): extract into diff func builder := txscript.NewScriptBuilder() builder.AddData(schnorr.SerializePubKey(selfKey)) builder.AddOp(txscript.OP_CHECKSIG) @@ -1822,10 +1824,26 @@ func NewLocalCommitScriptTree(csvTimeout uint32, return nil, err } - // With the delay script computed, we'll now create a tapscript tree - // with a single leaf, and then obtain a root from that. - tapLeaf := txscript.NewBaseTapLeaf(delayScript) - tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + // Next, we'll need to construct the revocation path, which is just a + // simple checksig script. + builder = txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(selfKey)) + builder.AddOp(txscript.OP_DROP) + builder.AddData(schnorr.SerializePubKey(revokeKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + revokeScript, err := builder.Script() + if err != nil { + return nil, err + } + + // With both scripts computed, we'll now create a tapscript tree with + // the two leaves, and then obtain a root from that. + delayTapLeaf := txscript.NewBaseTapLeaf(delayScript) + revokeTapLeaf := txscript.NewBaseTapLeaf(revokeScript) + tapScriptTree := txscript.AssembleTaprootScriptTree( + delayTapLeaf, revokeTapLeaf, + ) tapScriptRoot := tapScriptTree.RootNode.TapHash() // Now that we have our root, we can arrive at the final output script @@ -1835,16 +1853,19 @@ func NewLocalCommitScriptTree(csvTimeout uint32, ) return &CommitScriptTree{ - SettleLeaf: tapLeaf, - TaprootKey: toLocalOutputKey, - TapscriptTree: tapScriptTree, - TapscriptRoot: tapScriptRoot[:], + SettleLeaf: delayTapLeaf, + RevocationLeaf: revokeTapLeaf, + TaprootKey: toLocalOutputKey, + TapscriptTree: tapScriptTree, + TapscriptRoot: tapScriptRoot[:], }, nil } // TaprootCommitScriptToSelf creates the taproot witness program that commits -// to the revocation (keyspend) and delay path (script path) in a single -// taproot output key. +// to the revocation (script path) and delay path (script path) in a single +// taproot output key. Both the delay script and the revocation script are part +// of the tapscript tree to ensure that the internal key is always revealed. +// This ensures that a 3rd party can always sweep the set of anchor outputs. // // For the delay path we have the following tapscript leaf script: // @@ -1858,12 +1879,20 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // Where the to_delay_script is listed above, and the delay_control_block // computed as: // -// delay_control_block = (output_key_y_parity | 0xc0) || revocationpubkey +// delay_control_block = (output_key_y_parity | 0xc0) || taproot_nums_key // -// The revocation key spend path will simply present a valid signature with the -// witness being just: +// The revocation path is simply: +// +// OP_CHECKSIG +// OP_CHECKSIG +// +// The revocation path can be spent with a control block similar to the above +// (but contains the hash of the other script), and with the following witness: // // +// +// We use a noop data push to ensure that the local public key is also revealed +// on chain, which enables the anchor output to be swept. func TaprootCommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { @@ -1881,7 +1910,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32, // sweep the settled taproot output after the delay has passed for a force // close. func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor, - sweepTx *wire.MsgTx, revokeKey *btcec.PublicKey, + sweepTx *wire.MsgTx, scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { // First, we'll need to construct a valid control block to execute the @@ -1919,19 +1948,37 @@ func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor, // TaprootCommitSpendRevoke constructs a valid witness allowing a node to sweep // the revoked taproot output of a malicious peer. func TaprootCommitSpendRevoke(signer Signer, signDesc *SignDescriptor, - revokeTx *wire.MsgTx) (wire.TxWitness, error) { + revokeTx *wire.MsgTx, + scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { - // For this spend type, we only need a single signature which'll be a - // keyspend using the revoke private key. - sweepSig, err := signer.SignOutputRaw(revokeTx, signDesc) + // First, we'll need to construct a valid control block to execute the + // leaf script for revocation path. + revokeTapleafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + revokeIdx := scriptTree.LeafProofIndex[revokeTapleafHash] + revokeMerkleProof := scriptTree.LeafMerkleProofs[revokeIdx] + revokeControlBlock := revokeMerkleProof.ToControlBlock( + &TaprootNUMSKey, + ) + + // With the control block created, we'll now generate the signature we + // need to authorize the spend. + revokeSig, err := signer.SignOutputRaw(revokeTx, signDesc) if err != nil { return nil, err } - // The witness stack in this case is pretty simple: we only need to - // specify the signature generated. - witnessStack := make(wire.TxWitness, 1) - witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + // The final witness stack will be: + // + // + witnessStack := make(wire.TxWitness, 3) + witnessStack[0] = maybeAppendSighash(revokeSig, signDesc.HashType) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = revokeControlBlock.ToBytes() + if err != nil { + return nil, err + } return witnessStack, nil } diff --git a/input/taproot_test.go b/input/taproot_test.go index 2745a842d7e..c6fead21e84 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -977,7 +977,6 @@ func localCommitSweepWitGen(sigHash txscript.SigHashType, return TaprootCommitSpendSuccess( signer, signDesc, spendTx, - commitScriptTree.revokeKey.PubKey(), commitScriptTree.TapscriptTree, ) } @@ -1000,17 +999,18 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType, KeyDesc: keychain.KeyDescriptor{ PubKey: revokeKey.PubKey(), }, + WitnessScript: commitScriptTree.RevocationLeaf.Script, Output: commitScriptTree.txOut, HashType: sigHash, InputIndex: 0, SigHashes: hashCache, - SignMethod: TaprootKeySpendSignMethod, - TapTweak: commitScriptTree.TapscriptRoot, + SignMethod: TaprootScriptSpendSignMethod, PrevOutputFetcher: prevOuts, } return TaprootCommitSpendRevoke( signer, signDesc, spendTx, + commitScriptTree.TapscriptTree, ) } } From 7ef2306fd9a7f7bf0ad0e41beaf2a134351c27dc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 23 May 2023 18:22:40 -0700 Subject: [PATCH 16/18] input: fix linter errors --- input/script_utils.go | 67 ++++++++++++++++---------------- input/taproot_test.go | 89 ++++++++++++++++++++++--------------------- 2 files changed, 79 insertions(+), 77 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 94e50c94e62..d954e0f7b10 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -47,8 +47,9 @@ const TaprootNUMSHex = "02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb" + var ( // TaprootNUMSKey is a NUMS key (nothing up my sleeves number) that has // no known private key. This was generated using the following script: - // https://github.com/lightninglabs/lightning-node-connect/tree/master/mailbox/numsgen), - // with the seed phrase "Lightning Simple Taproot". + // https://github.com/lightninglabs/lightning-node-connect/tree/ + // master/mailbox/numsgen, with the seed phrase "Lightning Simple + // Taproot". TaprootNUMSKey = mustParsePubKey(TaprootNUMSHex) ) @@ -573,7 +574,8 @@ func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // HtlcScriptTree holds the taproot output key, as well as the two script path // leaves that every taproot HTLC script depends on. type HtlcScriptTree struct { - // TaprootKey is the key that will be used to generate the taproot output. + // TaprootKey is the key that will be used to generate the taproot + // output. TaprootKey *btcec.PublicKey // SuccessTapLeaf is the tapleaf for the redemption path. @@ -1074,7 +1076,7 @@ func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, timeoutLeafScript, err := builder.Script() if err != nil { - return txscript.TapLeaf{}, nil + return txscript.TapLeaf{}, err } return txscript.NewBaseTapLeaf(timeoutLeafScript), nil @@ -1084,10 +1086,10 @@ func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, // path for an HTLC on the receiver's commitment transaction. This script // allows the receiver to redeem an HTLC with knowledge of the preimage: // -// OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 -// OP_EQUALVERIFY -// OP_CHECKSIGVERIFY -// OP_CHECKSIG +// OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 +// OP_EQUALVERIFY +// OP_CHECKSIGVERIFY +// OP_CHECKSIG func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, senderHtlcKey *btcec.PublicKey, paymentHash []byte) (txscript.TapLeaf, error) { @@ -1165,9 +1167,9 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, } // ReceiverHTLCScriptTaproot constructs the taproot witness program (schnor -// key) for an outgoing HTLC on the receiver's version of the commitment +// key) for an incoming HTLC on the receiver's version of the commitment // transaction. This method returns the top level tweaked public key that -// commits to both the script paths. From the PoV fo teh receiver, this is an +// commits to both the script paths. From the PoV for the receiver, this is an // accepted HTLC. // // The returned key commits to a tapscript tree with two possible paths: @@ -1183,7 +1185,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // OP_CHECKSIGVERIFY // OP_CHECKSIG // -// The timeout path can be be spent with a witness of: +// The timeout path can be spent with a witness of: // - // // The success path can be spent with a witness of: @@ -1232,7 +1234,8 @@ func ReceiverHTLCScriptTaprootRedeem(senderSig Signature, successControlBlock := successMerkleProof.ToControlBlock(revokeKey) // The final witness stack is: - // * + // * + // witnessStack := wire.TxWitness(make([][]byte, 5)) witnessStack[0] = maybeAppendSighash(senderSig, senderSigHash) witnessStack[1] = maybeAppendSighash(sweepSig, signDesc.HashType) @@ -1423,8 +1426,8 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey, func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, csvDelay uint32) (*txscript.IndexedTapScriptTree, error) { - // First grab the second level leaf script we need to create the top level - // output. + // First grab the second level leaf script we need to create the top + // level output. secondLevelTapLeaf, err := TaprootSecondLevelTapLeaf(delayKey, csvDelay) if err != nil { return nil, err @@ -1501,8 +1504,7 @@ func TaprootHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, } // TaprootHtlcSpendSuccess spends a second-level HTLC output via the redemption -// path. This should be used to sweep funds after the pre-image is known or the -// timeout has elapsed on the commitment transaction of the broadcaster. +// path. This should be used to sweep funds after the pre-image is known. // // NOTE: The caller MUST set the txn version, sequence number, and sign // descriptor's sig hash cache before invocation. @@ -1782,9 +1784,9 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } -// CommitScript holds the taproot output key (in this case the revocation key, -// or a NUMs point for the remote output) along with the tapscript leaf that -// can spend the output after a delay. +// CommitScriptTree holds the taproot output key (in this case the revocation +// key, or a NUMs point for the remote output) along with the tapscript leaf +// that can spend the output after a delay. type CommitScriptTree struct { // TaprootKey is the key that will be used to generate the taproot // output. @@ -1864,8 +1866,9 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // TaprootCommitScriptToSelf creates the taproot witness program that commits // to the revocation (script path) and delay path (script path) in a single // taproot output key. Both the delay script and the revocation script are part -// of the tapscript tree to ensure that the internal key is always revealed. -// This ensures that a 3rd party can always sweep the set of anchor outputs. +// of the tapscript tree to ensure that the internal key (the local delay key) +// is always revealed. This ensures that a 3rd party can always sweep the set +// of anchor outputs. // // For the delay path we have the following tapscript leaf script: // @@ -1883,7 +1886,7 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // // The revocation path is simply: // -// OP_CHECKSIG +// OP_DROP // OP_CHECKSIG // // The revocation path can be spent with a control block similar to the above @@ -2196,21 +2199,21 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, ) (*CommitScriptTree, error) { - // First, construct the remote party's tapscript they'll use to sweep their - // outputs. + // First, construct the remote party's tapscript they'll use to sweep + // their outputs. builder := txscript.NewScriptBuilder() builder.AddData(schnorr.SerializePubKey(remoteKey)) builder.AddOp(txscript.OP_CHECKSIG) builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) - delayScript, err := builder.Script() + remoteScript, err := builder.Script() if err != nil { return nil, err } // With this script constructed, we'll map that into a tapLeaf, then // make a new tapscript root from that. - tapLeaf := txscript.NewBaseTapLeaf(delayScript) + tapLeaf := txscript.NewBaseTapLeaf(remoteScript) tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) tapScriptRoot := tapScriptTree.RootNode.TapHash() @@ -2232,7 +2235,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // output on the commitment transaction for the remote party. For the top level // key spend, we'll use a NUMs key to ensure that only the script path can be // taken. Using a set NUMs key here also means that recovery solutions can scan -// the chain given knowledge of the public key fo the remote party. We then +// the chain given knowledge of the public key for the remote party. We then // commit to a single tapscript leaf that holds the normal CSV 1 delay // script. // @@ -2245,8 +2248,6 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // succeeds, which then enforces our 1 CSV. The true will remain on the stack, // causing the script to pass. If the CHECKSIG fails, then a 0 will remain on // the stack. -// -// TODO(roasbeef): double check here can't pass additional stack elements? func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, ) (*btcec.PublicKey, error) { @@ -2390,10 +2391,8 @@ func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } -// AnchorScriptTree holds all the contents needed to to sweep a taproot anchor +// AnchorScriptTree holds all the contents needed to sweep a taproot anchor // output on chain. -// -// TODO(roasbeef): refactor trees to reduce dedup type AnchorScriptTree struct { // TaprootKey is the key that will be used to generate the taproot // output. @@ -2412,7 +2411,9 @@ type AnchorScriptTree struct { // NewAnchorScriptTree makes a new script tree for an anchor output with the // passed anchor key. -func NewAnchorScriptTree(anchorKey *btcec.PublicKey) (*AnchorScriptTree, error) { +func NewAnchorScriptTree(anchorKey *btcec.PublicKey, +) (*AnchorScriptTree, error) { + // The main script used is just a OP_16 CSV (anyone can sweep after 16 // blocks). builder := txscript.NewScriptBuilder() diff --git a/input/taproot_test.go b/input/taproot_test.go index c6fead21e84..6263e3aa034 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -29,8 +29,6 @@ type testSenderHtlcScriptTree struct { rootHash []byte htlcAmt int64 - - lockTime int32 } func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { @@ -358,7 +356,7 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { valid: true, }, - // Invalid spend of timeout path, invalid reciever sig. + // Invalid spend of timeout path, invalid receiver sig. { name: "timeout spend invalid receiver sig", witnessGen: htlcSenderTimeoutWitnessGen( @@ -384,7 +382,9 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { } for i, testCase := range testCases { + i := i testCase := testCase + spendTxCopy := spendTx.Copy() t.Run(testCase.name, func(t *testing.T) { @@ -419,12 +419,10 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( htlcScriptTree.htlcTxOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, + spendTxCopy, 0, + txscript.StandardVerifyFlags, nil, hashCache, htlcScriptTree.htlcAmt, - txscript.NewCannedPrevOutputFetcher( - htlcScriptTree.htlcTxOut.PkScript, - htlcScriptTree.htlcAmt, - ), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) @@ -837,7 +835,8 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { valid: false, }, } - for i, testCase := range testCases { + for i, testCase := range testCases { //nolint:paralleltest + i := i testCase := testCase spendTxCopy := spendTx.Copy() @@ -877,12 +876,10 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( htlcScriptTree.htlcTxOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, + spendTxCopy, 0, + txscript.StandardVerifyFlags, nil, hashCache, htlcScriptTree.htlcAmt, - txscript.NewCannedPrevOutputFetcher( - htlcScriptTree.htlcTxOut.PkScript, - htlcScriptTree.htlcAmt, - ), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) @@ -930,6 +927,9 @@ func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { selfKey.PubKey(), ) } + if err != nil { + return nil, err + } pkScript, err := PayToTaprootScript(commitScriptTree.TaprootKey) if err != nil { @@ -995,11 +995,12 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType, }, } + revScript := commitScriptTree.RevocationLeaf.Script signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: revokeKey.PubKey(), }, - WitnessScript: commitScriptTree.RevocationLeaf.Script, + WitnessScript: revScript, Output: commitScriptTree.txOut, HashType: sigHash, InputIndex: 0, @@ -1135,7 +1136,8 @@ func TestTaprootCommitScriptToSelf(t *testing.T) { }, } - for i, testCase := range testCases { + for i, testCase := range testCases { //nolint:paralleltest + i := i testCase := testCase spendTxCopy := spendTx.Copy() @@ -1169,12 +1171,11 @@ func TestTaprootCommitScriptToSelf(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( commitScriptTree.txOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, - nil, hashCache, int64(commitScriptTree.selfAmt), - txscript.NewCannedPrevOutputFetcher( - commitScriptTree.txOut.PkScript, - int64(commitScriptTree.selfAmt), - ), + spendTxCopy, 0, + txscript.StandardVerifyFlags, nil, + hashCache, + int64(commitScriptTree.selfAmt), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) @@ -1312,7 +1313,8 @@ func TestTaprootCommitScriptRemote(t *testing.T) { }, } - for i, testCase := range testCases { + for i, testCase := range testCases { //nolint:paralleltest + i := i testCase := testCase spendTxCopy := spendTx.Copy() @@ -1346,12 +1348,11 @@ func TestTaprootCommitScriptRemote(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( commitScriptTree.txOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, - nil, hashCache, int64(commitScriptTree.selfAmt), - txscript.NewCannedPrevOutputFetcher( - commitScriptTree.txOut.PkScript, - int64(commitScriptTree.selfAmt), - ), + spendTxCopy, 0, + txscript.StandardVerifyFlags, nil, + hashCache, + int64(commitScriptTree.selfAmt), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) @@ -1531,7 +1532,8 @@ func TestTaprootAnchorScript(t *testing.T) { }, } - for i, testCase := range testCases { + for i, testCase := range testCases { //nolint:paralleltest + i := i testCase := testCase spendTxCopy := spendTx.Copy() @@ -1565,12 +1567,11 @@ func TestTaprootAnchorScript(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( anchorScriptTree.txOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, - nil, hashCache, int64(anchorScriptTree.amt), - txscript.NewCannedPrevOutputFetcher( - anchorScriptTree.txOut.PkScript, - int64(anchorScriptTree.amt), - ), + spendTxCopy, 0, + txscript.StandardVerifyFlags, + nil, hashCache, + int64(anchorScriptTree.amt), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) @@ -1799,7 +1800,7 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { }, { - name: "valid revocation sweep sig hash deafult", + name: "valid revocation sweep sig hash default", witnessGen: secondLevelHtlcRevokeWitnessgen( txscript.SigHashDefault, htlcScriptTree, ), @@ -1828,7 +1829,8 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { }, } - for i, testCase := range testCases { + for i, testCase := range testCases { //nolint:paralleltest + i := i testCase := testCase spendTxCopy := spendTx.Copy() @@ -1862,12 +1864,11 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( htlcScriptTree.txOut.PkScript, - spendTxCopy, 0, txscript.StandardVerifyFlags, - nil, hashCache, int64(htlcScriptTree.amt), - txscript.NewCannedPrevOutputFetcher( - htlcScriptTree.txOut.PkScript, - int64(htlcScriptTree.amt), - ), + spendTxCopy, 0, + txscript.StandardVerifyFlags, + nil, hashCache, + int64(htlcScriptTree.amt), + prevOuts, ) } assertEngineExecution(t, i, testCase.valid, newEngine) From 34a9cfba9060214e7b9628f1ce1c6ce80092d3a5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 24 May 2023 20:57:20 -0700 Subject: [PATCH 17/18] input: use explicit CSV 1 script for to remote output We undo the prior hack here to make the final script more readable. The difference is just 1 byte between the two. --- input/script_utils.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index d954e0f7b10..ae9a6f02f5c 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -2204,7 +2204,9 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, builder := txscript.NewScriptBuilder() builder.AddData(schnorr.SerializePubKey(remoteKey)) builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_1) builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) remoteScript, err := builder.Script() if err != nil { @@ -2242,12 +2244,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // Our single tapleaf will use the following script: // // OP_CHECKSIG -// OP_CHECKSEQUENCEVERIFY -// -// The CSV clause is a bit subtle, but OP_CHECKSIG will return true if it -// succeeds, which then enforces our 1 CSV. The true will remain on the stack, -// causing the script to pass. If the CHECKSIG fails, then a 0 will remain on -// the stack. +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, ) (*btcec.PublicKey, error) { From 388a70c41ff6732f9cbe44b13b81f7ab3eae222e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 24 May 2023 21:13:41 -0700 Subject: [PATCH 18/18] input: eliminate CSV trick for HTLC outputs --- input/script_utils.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index ae9a6f02f5c..2b18780dbc7 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -540,7 +540,7 @@ func SenderHTLCTapLeafTimeout(senderHtlcKey, // OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 // OP_EQUALVERIFY // OP_CHECKSIG -// OP_CHECKSEQUENCEVERIFY +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, paymentHash []byte) (txscript.TapLeaf, error) { @@ -561,7 +561,9 @@ func SenderHTLCTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // after confirmation to properly sweep. builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_1) builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) successLeafScript, err := builder.Script() if err != nil { @@ -650,7 +652,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // OP_SIZE 32 OP_EQUALVERIFY // OP_HASH160 OP_EQUALVERIFY // OP_CHECKSIG -// OP_CHECKSEQUENCEVERIFY +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP // // The timeout path can be spent with a witness of (sender timeout): // @@ -1055,7 +1057,7 @@ func ReceiverHtlcSpendTimeout(signer Signer, signDesc *SignDescriptor, // timeout the HTLC after expiry: // // OP_CHECKSIG -// OP_CHECKSEQUENCEVERIFY +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP // OP_CHECKLOCKTIMEVERIFY OP_DROP func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, cltvExpiry uint32) (txscript.TapLeaf, error) { @@ -1066,7 +1068,9 @@ func ReceiverHtlcTapLeafTimeout(senderHtlcKey *btcec.PublicKey, // sender authorizing the spend (the timeout). builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) builder.AddOp(txscript.OP_CHECKSIG) + builder.AddOp(txscript.OP_1) builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) // The second portion will ensure that the CLTV expiry on the spending // transaction is correct. @@ -1176,7 +1180,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // // - The timeout path: // OP_CHECKSIG -// OP_CHECKSEQUENCEVERIFY +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP // OP_CHECKLOCKTIMEVERIFY OP_DROP // // - Success path: