diff --git a/go.mod b/go.mod index 1cda535..e6d3223 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,11 @@ require ( github.com/btcsuite/btcwallet/wtxmgr v1.5.0 github.com/coreos/bbolt v1.3.3 github.com/davecgh/go-spew v1.1.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/fortytw2/leaktest v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/jessevdk/go-flags v1.4.0 - github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f + github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38 github.com/lightninglabs/lndclient v0.15.0-0 github.com/lightninglabs/loop/swapserverrpc v1.0.0 github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display diff --git a/go.sum b/go.sum index e9deee2..d162232 100644 --- a/go.sum +++ b/go.sum @@ -472,8 +472,8 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f h1:wfyZ3sZXoGayB+V8icHl6uoqOX6wiyncKFK9pTleGRw= -github.com/lightninglabs/aperture v0.1.17-beta.0.20220325093943-42b9d4c1be7f/go.mod h1:lDjRKhndRH0CzZQ2m8dWODdqp/ejEW7esb2u2nlvrw4= +github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38 h1:ht1wzuSmScXJEnW30Cp2vUBHYzJZvme5DHxNMyg+LT0= +github.com/lightninglabs/aperture v0.1.17-beta.0.20220328072456-4a2632d0be38/go.mod h1:lDjRKhndRH0CzZQ2m8dWODdqp/ejEW7esb2u2nlvrw4= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= diff --git a/loopin.go b/loopin.go index c3c002f..63187e2 100644 --- a/loopin.go +++ b/loopin.go @@ -966,7 +966,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context, } witnessFunc := func(sig []byte) (wire.TxWitness, error) { - return s.htlc.GenTimeoutWitness(sig), nil + return s.htlc.GenTimeoutWitness(sig) } sequence := uint32(0) diff --git a/swap/htlc.go b/swap/htlc.go index b6e5606..24d9434 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -7,17 +7,26 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" ) -// ErrNoSharedKey is returned when a script version does not support use of a -// shared key. -var ErrNoSharedKey = errors.New("shared key not supported for script version") +var ( + // ErrNoSharedKey is returned when a script version does not support + // use of a shared key. + ErrNoSharedKey = errors.New("shared key not supported for script " + + "version") + + // ErrSharedKeyRequired is returned when a script version requires a + // shared key. + ErrSharedKeyRequired = errors.New("shared key required") +) // HtlcOutputType defines the output type of the htlc that is published. type HtlcOutputType uint8 @@ -29,6 +38,9 @@ const ( // HtlcNP2WSH is a nested pay-to-witness-script-hash output that can be // paid to be legacy wallets. HtlcNP2WSH + + // HtlcP2TR is a pay-to-taproot output with three separate spend paths. + HtlcP2TR ) // ScriptVersion defines the HTLC script version. @@ -40,17 +52,21 @@ const ( // HtlcV2 refers to the improved version of the HTLC script. HtlcV2 + + // HtlcV3 refers to an upgraded version of HtlcV2 implemented with + // tapscript. + HtlcV3 ) // htlcScript defines an interface for the different HTLC implementations. type HtlcScript interface { // genSuccessWitness returns the success script to spend this htlc with // the preimage. - genSuccessWitness(receiverSig []byte, preimage lntypes.Preimage) wire.TxWitness + genSuccessWitness(receiverSig []byte, preimage lntypes.Preimage) (wire.TxWitness, error) // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. - GenTimeoutWitness(senderSig []byte) wire.TxWitness + GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) // IsSuccessWitness checks whether the given stack is valid for // redeeming the htlc. @@ -111,12 +127,17 @@ func (h HtlcOutputType) String() string { case HtlcNP2WSH: return "NP2WSH" + case HtlcP2TR: + return "P2TR" + default: return "unknown" } } -// NewHtlc returns a new instance. +// NewHtlc returns a new instance. For V1 and V2 scripts, receiver and sender +// keys are expected to be in ecdsa compressed format. For V3 scripts, keys are +// expected to be raw private key bytes. func NewHtlc(version ScriptVersion, cltvExpiry int32, senderKey, receiverKey [33]byte, sharedKey *btcec.PublicKey, hash lntypes.Hash, outputType HtlcOutputType, @@ -145,6 +166,14 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, htlc, err = newHTLCScriptV2( cltvExpiry, senderKey, receiverKey, hash, ) + case HtlcV3: + if sharedKey == nil { + return nil, ErrSharedKeyRequired + } + + htlc, err = newHTLCScriptV3( + cltvExpiry, senderKey, receiverKey, sharedKey, hash, + ) default: return nil, ErrInvalidScriptVersion @@ -154,18 +183,18 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, return nil, err } - p2wshPkScript, err := input.WitnessScriptHash(htlc.Script()) - if err != nil { - return nil, err - } - var pkScript, sigScript []byte var address btcutil.Address switch outputType { case HtlcNP2WSH: + pks, err := input.WitnessScriptHash(htlc.Script()) + if err != nil { + return nil, err + } + // Generate p2sh script for p2wsh (nested). - p2wshPkScriptHash := sha256.Sum256(p2wshPkScript) + p2wshPkScriptHash := sha256.Sum256(pks) hash160 := input.Ripemd160H(p2wshPkScriptHash[:]) builder := txscript.NewScriptBuilder() @@ -184,29 +213,53 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, // the p2wsh witness program corresponding to the matching // public key of this address. sigScript, err = txscript.NewScriptBuilder(). - AddData(p2wshPkScript). + AddData(pks). Script() if err != nil { return nil, err } address, err = btcutil.NewAddressScriptHash( - p2wshPkScript, chainParams, + pkScript, chainParams, ) if err != nil { return nil, err } case HtlcP2WSH: - pkScript = p2wshPkScript + pkScript, err = input.WitnessScriptHash(htlc.Script()) + if err != nil { + return nil, err + } address, err = btcutil.NewAddressWitnessScriptHash( - p2wshPkScript[2:], + pkScript[2:], chainParams, ) if err != nil { return nil, err } + case HtlcP2TR: + // Confirm we have a v3 htlc. + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.(*HtlcScriptV3) + if !ok { + return nil, errors.New("taproot output selected for nontaproot htlc") + } + + // Generate a tapscript address from our tree. + address, err = btcutil.NewAddressTaproot( + schnorr.SerializePubKey(trHtlc.TaprootKey), &chaincfg.RegressionNetParams, + ) + if err != nil { + return nil, err + } + + // Generate locking script. + pkScript, err = txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } default: return nil, errors.New("unknown output type") } @@ -232,7 +285,7 @@ func (h *Htlc) GenSuccessWitness(receiverSig []byte, return nil, errors.New("preimage doesn't match hash") } - return h.genSuccessWitness(receiverSig, preimage), nil + return h.genSuccessWitness(receiverSig, preimage) } // AddSuccessToEstimator adds a successful spend to a weight estimator. @@ -322,26 +375,27 @@ func newHTLCScriptV1(cltvExpiry int32, senderHtlcKey, // genSuccessWitness returns the success script to spend this htlc with // the preimage. func (h *HtlcScriptV1) genSuccessWitness(receiverSig []byte, - preimage lntypes.Preimage) wire.TxWitness { + preimage lntypes.Preimage) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = append(receiverSig, byte(txscript.SigHashAll)) witnessStack[1] = preimage[:] witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. -func (h *HtlcScriptV1) GenTimeoutWitness(senderSig []byte) wire.TxWitness { +func (h *HtlcScriptV1) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = append(senderSig, byte(txscript.SigHashAll)) witnessStack[1] = []byte{0} witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // IsSuccessWitness checks whether the given stack is valid for redeeming the @@ -456,14 +510,14 @@ func newHTLCScriptV2(cltvExpiry int32, senderHtlcKey, // genSuccessWitness returns the success script to spend this htlc with // the preimage. func (h *HtlcScriptV2) genSuccessWitness(receiverSig []byte, - preimage lntypes.Preimage) wire.TxWitness { + preimage lntypes.Preimage) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 3) witnessStack[0] = preimage[:] witnessStack[1] = append(receiverSig, byte(txscript.SigHashAll)) witnessStack[2] = h.script - return witnessStack + return witnessStack, nil } // IsSuccessWitness checks whether the given stack is valid for redeeming the @@ -476,7 +530,8 @@ func (h *HtlcScriptV2) IsSuccessWitness(witness wire.TxWitness) bool { // GenTimeoutWitness returns the timeout script to spend this htlc after // timeout. -func (h *HtlcScriptV2) GenTimeoutWitness(senderSig []byte) wire.TxWitness { +func (h *HtlcScriptV2) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { witnessStack := make(wire.TxWitness, 4) witnessStack[0] = append(senderSig, byte(txscript.SigHashAll)) @@ -484,7 +539,7 @@ func (h *HtlcScriptV2) GenTimeoutWitness(senderSig []byte) wire.TxWitness { witnessStack[2] = []byte{} witnessStack[3] = h.script - return witnessStack + return witnessStack, nil } // Script returns the htlc script. @@ -525,3 +580,207 @@ func (h *HtlcScriptV2) MaxTimeoutWitnessSize() int { func (h *HtlcScriptV2) SuccessSequence() uint32 { return 1 } + +// HtlcScriptV3 encapsulates the htlc v3 script. +type HtlcScriptV3 struct { + timeoutScript []byte + claimScript []byte + TaprootKey *secp.PublicKey + InternalPubKey *secp.PublicKey + SenderKey [33]byte +} + +// newHTLCScriptV3 constructs a HtlcScipt with the HTLC V3 taproot script. +func newHTLCScriptV3(cltvExpiry int32, senderHtlcKey, + receiverHtlcKey [33]byte, sharedKey *btcec.PublicKey, + swapHash lntypes.Hash) (*HtlcScriptV3, error) { + + // Schnorr keys have implicit sign, remove the sign byte from our + // compressed key. + var schnorrSenderKey, schnorrReceiverKey [32]byte + copy(schnorrSenderKey[:], senderHtlcKey[1:]) + copy(schnorrReceiverKey[:], receiverHtlcKey[1:]) + + // Create our claim path script, we'll use this separately + // later on to generate the claim path leaf. + claimPathScript, err := GenClaimPathScript(schnorrReceiverKey, swapHash) + if err != nil { + return nil, err + } + + // Create our timeout path leaf, we'll use this separately + // later on to generate the timeout path leaf. + timeoutPathScript, err := GenTimeoutPathScript( + schnorrSenderKey, int64(cltvExpiry), + ) + if err != nil { + return nil, err + } + + // Assemble our taproot script tree from our leaves. + tree := txscript.AssembleTaprootScriptTree( + txscript.NewBaseTapLeaf(claimPathScript), + txscript.NewBaseTapLeaf(timeoutPathScript), + ) + + rootHash := tree.RootNode.TapHash() + + // Calculate top level taproot key, using our shared key as the internal + // key. + taprootKey := txscript.ComputeTaprootOutputKey( + sharedKey, rootHash[:], + ) + + return &HtlcScriptV3{ + timeoutScript: timeoutPathScript, + claimScript: claimPathScript, + TaprootKey: taprootKey, + InternalPubKey: sharedKey, + SenderKey: senderHtlcKey, + }, nil +} + +// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path. +// +// OP_CHECKSIGVERIFY OP_CHECKLOCKTIMEVERIFY +func GenTimeoutPathScript( + senderHtlcKey [32]byte, cltvExpiry int64) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + builder.AddData(senderHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(cltvExpiry) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + return builder.Script() +} + +// GenClaimPathScript constructs an HtlcScript for the claim payment path. +// +// OP_CHECKSIGVERIFY +// OP_SIZE 32 OP_EQUALVERIFY +// OP_HASH160 OP_EQUALVERIFY +// 1 +// OP_CHECKSEQUENCEVERIFY +func GenClaimPathScript( + receiverHtlcKey [32]byte, swapHash lntypes.Hash) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + builder.AddData(receiverHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddOp(txscript.OP_HASH160) + builder.AddData(input.Ripemd160H(swapHash[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddInt64(1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + return builder.Script() +} + +// genControlBlock constructs the control block with the specified. +func (h *HtlcScriptV3) genControlBlock(leafScript []byte) ([]byte, error) { + var outputKeyYIsOdd bool + + if h.TaprootKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd { + outputKeyYIsOdd = true + } + + leaf := txscript.NewBaseTapLeaf(leafScript) + proof := leaf.TapHash() + + controlBlock := txscript.ControlBlock{ + InternalKey: h.InternalPubKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: proof[:], + } + controlBlockBytes, err := controlBlock.ToBytes() + + if err != nil { + return nil, err + } + return controlBlockBytes, nil +} + +// genSuccessWitness returns the success script to spend this htlc with +// the preimage. +func (h *HtlcScriptV3) genSuccessWitness( + receiverSig []byte, preimage lntypes.Preimage) (wire.TxWitness, error) { + + controlBlockBytes, err := h.genControlBlock(h.timeoutScript) + if err != nil { + return nil, err + } + return wire.TxWitness{ + preimage[:], + receiverSig, + h.claimScript, + controlBlockBytes, + }, nil +} + +// GenTimeoutWitness returns the timeout script to spend this htlc after +// timeout. +func (h *HtlcScriptV3) GenTimeoutWitness( + senderSig []byte) (wire.TxWitness, error) { + + controlBlockBytes, err := h.genControlBlock(h.claimScript) + if err != nil { + return nil, err + } + return wire.TxWitness{ + senderSig, + h.timeoutScript, + controlBlockBytes, + }, nil +} + +// IsSuccessWitness checks whether the given stack is valid for +// redeeming the htlc. +func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { + return len(witness) == 4 +} + +// Script is not implemented, but necessary to conform to interface. +func (h *HtlcScriptV3) Script() []byte { + return nil +} + +// MaxSuccessWitnessSize returns the maximum witness size for the +// success case witness. +func (h *HtlcScriptV3) MaxSuccessWitnessSize() int { + // Calculate maximum success witness size + // + // - number_of_witness_elements: 1 byte + // - sig_length: 1 byte + // - sig: 73 bytes + // - preimage_length: 1 byte + // - preimage: 32 bytes + // - witness_script_length: 1 byte + // - witness_script: len(script) bytes + // - control_block_length: 1 byte + // - control_block: 4129 bytes + return 1 + 1 + 73 + 1 + 32 + 1 + len(h.claimScript) + 1 + 4129 +} + +// MaxTimeoutWitnessSize returns the maximum witness size for the +// timeout case witness. +func (h *HtlcScriptV3) MaxTimeoutWitnessSize() int { + // Calculate maximum timeout witness size + // + // - number_of_witness_elements: 1 byte + // - sig_length: 1 byte + // - sig: 73 bytes + // - witness_script_length: 1 byte + // - witness_script: len(script) bytes + // - control_block_length: 1 byte + // - control_block: 4129 bytes + return 1 + 1 + 73 + 1 + len(h.timeoutScript) + 1 + 4129 +} + +// SuccessSequence returns the sequence to spend this htlc in the +// success case. +func (h *HtlcScriptV3) SuccessSequence() uint32 { + return 1 +} diff --git a/swap/htlc_test.go b/swap/htlc_test.go index 1202b0f..75b8c2a 100644 --- a/swap/htlc_test.go +++ b/swap/htlc_test.go @@ -7,11 +7,13 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -101,6 +103,8 @@ func TestHtlcV2(t *testing.T) { var ( testPreimage = lntypes.Preimage([32]byte{1, 2, 3}) err error + receiverKey [33]byte + senderKey [33]byte ) // We generate a fake output, and the corresponding txin. This output @@ -125,12 +129,8 @@ func TestHtlcV2(t *testing.T) { senderPrivKey, senderPubKey := test.CreateKey(1) receiverPrivKey, receiverPubKey := test.CreateKey(2) - var ( - senderKey [33]byte - receiverKey [33]byte - ) - copy(senderKey[:], senderPubKey.SerializeCompressed()) copy(receiverKey[:], receiverPubKey.SerializeCompressed()) + copy(senderKey[:], senderPubKey.SerializeCompressed()) hash := sha256.Sum256(testPreimage[:]) @@ -155,6 +155,9 @@ func TestHtlcV2(t *testing.T) { receiverSigner := &input.MockSigner{ Privkeys: []*btcec.PrivateKey{receiverPrivKey}, } + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + htlc.PkScript, 800_000, + ) signTx := func(tx *wire.MsgTx, pubkey *btcec.PublicKey, signer *input.MockSigner) (input.Signature, error) { @@ -167,8 +170,10 @@ func TestHtlcV2(t *testing.T) { WitnessScript: htlc.Script(), Output: htlcOutput, HashType: txscript.SigHashAll, - SigHashes: txscript.NewTxSigHashes(tx), - InputIndex: 0, + SigHashes: txscript.NewTxSigHashes( + tx, prevOutFetcher, + ), + InputIndex: 0, } return signer.SignOutputRaw(tx, signDesc) @@ -227,9 +232,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, { @@ -242,9 +250,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, true, }, { @@ -257,9 +268,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, { @@ -272,7 +286,7 @@ func TestHtlcV2(t *testing.T) { // Create the htlc with the bogus key. htlc, err = NewHtlc( HtlcV2, testCltvExpiry, - bogusKey, receiverKey, hash, + bogusKey, receiverKey, nil, hash, HtlcP2WSH, &chaincfg.MainNetParams, ) require.NoError(t, err) @@ -289,9 +303,12 @@ func TestHtlcV2(t *testing.T) { ) require.NoError(t, err) - return htlc.GenTimeoutWitness( + witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) + require.NoError(t, err) + + return witness }, false, }, } @@ -306,8 +323,281 @@ func TestHtlcV2(t *testing.T) { return txscript.NewEngine( htlc.PkScript, sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(htlcValue), + nil, int64(htlcValue), prevOutFetcher, + ) + } + + assertEngineExecution(t, testCase.valid, newEngine) + }) + } +} + +// TestHtlcV3 tests the HTLC V3 script success and timeout spend cases. +// TODO: update tests to use private keys, not public keys and use non-nil +// shared key. +func TestHtlcV3(t *testing.T) { + preimage := [32]byte{1, 2, 3} + p := lntypes.Preimage(preimage) + hashedPreimage := sha256.Sum256(p[:]) + value := int64(800_000 - 500) // TODO(guggero): Calculate actual fee. + + senderPrivKey := [33]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, byte(1), + } + receiverPrivKey := [33]byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, byte(2), + } + cltvExpiry := int32(10) + + htlc, err := NewHtlc( + HtlcV3, cltvExpiry, senderPrivKey, receiverPrivKey, nil, + hashedPreimage, HtlcP2TR, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + var trAddress *btcutil.AddressTaproot + trAddress, ok := htlc.Address.(*btcutil.AddressTaproot) + require.True(t, ok) + + p2trPkScript, err := txscript.PayToAddrScript(trAddress) + require.NoError(t, err) + + tx := wire.NewMsgTx(2) + tx.TxIn = []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash(sha256.Sum256([]byte{1, 2, 3})), + Index: 50, + }, + }} + tx.TxOut = []*wire.TxOut{{ + PkScript: []byte{0, 20, 2, 141, 221, 230, 144, 171, 89, 230, 219, 198, 90, 157, 110, 89, 89, 67, 128, 16, 150, 186}, + Value: value, + }} + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + p2trPkScript, 800_000, + ) + hashCache := txscript.NewTxSigHashes( + tx, prevOutFetcher, + ) + + signTx := func( + tx *wire.MsgTx, privateKey *secp.PrivateKey, leaf txscript.TapLeaf) []byte { + + sig, err := txscript.RawTxInTapscriptSignature( + tx, hashCache, 0, value, p2trPkScript, leaf, + txscript.SigHashDefault, privateKey, + ) + require.NoError(t, err) + + return sig + } + + testCases := []struct { + name string + witness func(*testing.T) wire.TxWitness + valid bool + }{ + { + // Receiver can spend with valid preimage. + "success case spend with valid preimage", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + senderPrivKey, _ := btcec.PrivKeyFromBytes( + senderPrivKey[:], + ) + + sig := signTx( + tx, senderPrivKey, txscript.NewBaseTapLeaf(trHtlc.claimScript), ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, true, + }, + { + // Receiver can't spend with the valid preimage and with + // zero sequence. + "success case no spend with valid preimage and zero sequence", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = 0 + + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + senderPrivKey, _ := btcec.PrivKeyFromBytes( + senderPrivKey[:], + ) + + sig := signTx( + tx, senderPrivKey, txscript.NewBaseTapLeaf(trHtlc.claimScript), + ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can't spend when haven't yet timed out. + "timeout case no spend before timeout", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) - 1 + + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + receiverPrivKey, _ := btcec.PrivKeyFromBytes( + receiverPrivKey[:], + ) + + sig := signTx( + tx, receiverPrivKey, txscript.NewBaseTapLeaf(trHtlc.timeoutScript), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can spend after timeout. + "timeout case spend after timeout", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + receiverPrivKey, _ := btcec.PrivKeyFromBytes( + receiverPrivKey[:], + ) + + sig := signTx( + tx, receiverPrivKey, txscript.NewBaseTapLeaf(trHtlc.timeoutScript), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, true, + }, + { + // Receiver can't spend after timeout. + "timeout case receiver cannot spend", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = htlc.SuccessSequence() + tx.LockTime = uint32(cltvExpiry) + + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) + require.True(t, ok) + + senderPrivKey, _ := btcec.PrivKeyFromBytes( + senderPrivKey[:], + ) + + sig := signTx( + tx, senderPrivKey, txscript.NewBaseTapLeaf(trHtlc.timeoutScript), + ) + + witness, err := htlc.GenTimeoutWitness(sig) + require.NoError(t, err) + + return witness + }, false, + }, + { + // Sender can't spend after timeout with wrong sender + // key. + "timeout case cannot spend with wrong key", + func(t *testing.T) wire.TxWitness { + bogusKey := [33]byte{0xb, 0xa, 0xd} + + senderPrivKey, senderPubKey := btcec.PrivKeyFromBytes( + senderPrivKey[:], + ) + var shnorrSenderKey [32]byte + copy( + shnorrSenderKey[:], + schnorr.SerializePubKey(senderPubKey), + ) + + htlc, err := NewHtlc( + HtlcV3, cltvExpiry, bogusKey, + receiverPrivKey, nil, + hashedPreimage, HtlcP2TR, + &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + var trAddress *btcutil.AddressTaproot + trAddress, ok := htlc.Address.(*btcutil.AddressTaproot) + require.True(t, ok) + + p2trPkScript, err := txscript.PayToAddrScript(trAddress) + require.NoError(t, err) + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + p2trPkScript, 800_000, + ) + hashCache = txscript.NewTxSigHashes( + tx, prevOutFetcher, + ) + + timeoutScript, err := GenTimeoutPathScript( + shnorrSenderKey, int64(cltvExpiry), + ) + require.NoError(t, err) + + sig := signTx( + tx, senderPrivKey, txscript.NewBaseTapLeaf(timeoutScript), + ) + witness, err := htlc.genSuccessWitness( + sig, preimage, + ) + require.NoError(t, err) + + return witness + }, false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + tx.TxIn[0].Witness = testCase.witness(t) + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + p2trPkScript, tx, 0, + txscript.StandardVerifyFlags, nil, + hashCache, value, prevOutFetcher) } assertEngineExecution(t, testCase.valid, newEngine)