diff --git a/input/script_utils.go b/input/script_utils.go index d625e69dc03..2b18780dbc7 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -3,9 +3,12 @@ package input import ( "bytes" "crypto/sha256" + "encoding/hex" "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" "github.com/btcsuite/btcd/wire" @@ -21,6 +24,35 @@ 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 { @@ -141,6 +173,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, @@ -443,6 +509,277 @@ 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 +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP +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-image 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_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + successLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + return txscript.NewBaseTapLeaf(successLeafScript), nil +} + +// 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 *btcec.PublicKey + + // SuccessTapLeaf is the tapleaf for the redemption path. + SuccessTapLeaf txscript.TapLeaf + + // TimeoutTapLeaf is the tapleaf for the timeout path. + TimeoutTapLeaf 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 +} + +// 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, + TapscriptRoot: tapScriptRoot[:], + }, 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 +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP +// +// 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, + ) +} + +// 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. 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) { + + 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] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[1] = preimage + witnessStack[2] = signDesc.WitnessScript + witnessStack[3], 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. 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, + 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] = maybeAppendSighash(receiverSig, receiverSigHash) + witnessStack[1] = maybeAppendSighash(sweepSig, 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] = maybeAppendSighash(sweepSig, 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: @@ -715,6 +1052,277 @@ 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 +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP +// OP_CHECKLOCKTIMEVERIFY OP_DROP +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 + // 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. + builder.AddInt64(int64(cltvExpiry)) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + builder.AddOp(txscript.OP_DROP) + + timeoutLeafScript, err := builder.Script() + if err != nil { + return txscript.TapLeaf{}, err + } + + 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-image 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(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) + 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( + senderHtlcKey, 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( + timeoutTapLeaf, successTapLeaf, + ) + + 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, + TapscriptRoot: tapScriptRoot[:], + }, nil +} + +// ReceiverHTLCScriptTaproot constructs the taproot witness program (schnor +// 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 for the receiver, this is an +// accepted HTLC. +// +// The returned key commits to a tapscript tree with two possible paths: +// +// - The timeout path: +// OP_CHECKSIG +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP +// OP_CHECKLOCKTIMEVERIFY OP_DROP +// +// - Success path: +// OP_SIZE 32 OP_EQUALVERIFY +// OP_HASH160 OP_EQUALVERIFY +// OP_CHECKSIGVERIFY +// OP_CHECKSIG +// +// The timeout path can 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, + ) +} + +// 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] = maybeAppendSighash(senderSig, senderSigHash) + witnessStack[1] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[2] = paymentPreimage + witnessStack[3] = signDesc.WitnessScript + witnessStack[4], err = successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return witnessStack, nil +} + +// 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) { + + // 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. + timeoutTapLeafHash := txscript.NewBaseTapLeaf( + signDesc.WitnessScript, + ).TapHash() + 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] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], err = timeoutControlBlock.ToBytes() + if err != nil { + return nil, err + } + + 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 @@ -784,6 +1392,162 @@ 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 +} + +// 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] = maybeAppendSighash(sweepSig, 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. +// +// 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[0] = maybeAppendSighash(sweepSig, signDesc.HashType) + witnessStack[1] = signDesc.WitnessScript + witnessStack[2], 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 @@ -1024,6 +1788,208 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } +// 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. + TaprootKey *btcec.PublicKey + + // 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 + + // 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. + 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 + } + + // 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 + // by tweaking the internal key with this root. + toLocalOutputKey := txscript.ComputeTaprootOutputKey( + &TaprootNUMSKey, tapScriptRoot[:], + ) + + return &CommitScriptTree{ + SettleLeaf: delayTapLeaf, + RevocationLeaf: revokeTapLeaf, + TaprootKey: toLocalOutputKey, + TapscriptTree: tapScriptTree, + TapscriptRoot: tapScriptRoot[:], + }, nil +} + +// 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 (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: +// +// 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) || taproot_nums_key +// +// The revocation path is simply: +// +// OP_DROP +// 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) { + + commitScriptTree, err := NewLocalCommitScriptTree( + csvTimeout, selfKey, revokeKey, + ) + if err != nil { + return nil, err + } + + 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, + 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( + &TaprootNUMSKey, + ) + + // 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 +} + +// 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, + scriptTree *txscript.IndexedTapScriptTree) (wire.TxWitness, error) { + + // 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 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 +} + // 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 @@ -1232,6 +2198,104 @@ 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(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_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + 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(remoteScript) + 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( + &TaprootNUMSKey, 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 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 for 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: +// +// OP_CHECKSIG +// 1 OP_CHECKSEQUENCEVERIFY OP_DROP +func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, +) (*btcec.PublicKey, error) { + + commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey) + if err != nil { + return nil, err + } + + return commitScriptTree.TaprootKey, nil +} + +// 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, + 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(&TaprootNUMSKey) + + // 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 // the commitment transaction paying to the remote party of said commitment // transaction. The money can only be spend after one confirmation. @@ -1328,6 +2392,127 @@ func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { return builder.Script() } +// AnchorScriptTree holds all the contents needed to sweep a taproot anchor +// output on chain. +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() + 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. + anchorOutputKey := txscript.ComputeTaprootOutputKey( + anchorKey, tapScriptRoot[:], + ) + + 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 // anchor output on the commitment transaction using their funding key. This is // used for the anchor channel type. 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.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() +} diff --git a/input/taproot_test.go b/input/taproot_test.go new file mode 100644 index 00000000000..6263e3aa034 --- /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 +} + +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 receiver 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 { + i := i + 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, + prevOuts, + ) + } + 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 { //nolint:paralleltest + i := i + 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, + prevOuts, + ) + } + 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( + selfKey.PubKey(), + ) + } + if err != nil { + return nil, err + } + + 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.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, + }, + } + + revScript := commitScriptTree.RevocationLeaf.Script + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: revokeKey.PubKey(), + }, + WitnessScript: revScript, + Output: commitScriptTree.txOut, + HashType: sigHash, + InputIndex: 0, + SigHashes: hashCache, + SignMethod: TaprootScriptSpendSignMethod, + PrevOutputFetcher: prevOuts, + } + + return TaprootCommitSpendRevoke( + signer, signDesc, spendTx, + commitScriptTree.TapscriptTree, + ) + } +} + +// 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 { //nolint:paralleltest + i := i + 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), + prevOuts, + ) + } + 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.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 { //nolint:paralleltest + i := i + 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), + prevOuts, + ) + } + 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 { //nolint:paralleltest + i := i + 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), + prevOuts, + ) + } + 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 default", + 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 { //nolint:paralleltest + i := i + 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), + prevOuts, + ) + } + 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 }