package swap import ( "bytes" "crypto/sha256" "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" "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" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) // assertEngineExecution executes the VM returned by the newEngine closure, // asserting the result matches the validity expectation. In the case where it // doesn't match the expectation, it executes the script step-by-step and // prints debug information to stdout. // This code is adopted from: lnd/input/script_utils_test.go . func assertEngineExecution(t *testing.T, valid bool, newEngine func() (*txscript.Engine, error)) { t.Helper() // Get a new VM to execute. vm, err := newEngine() require.NoError(t, err, "unable to create engine") // Execute the VM, only go on to the step-by-step execution if it // doesn't validate as expected. vmErr := vm.Execute() executionValid := vmErr == nil if valid == executionValid { return } // Now that the execution didn't match what we expected, fetch a new VM // to step through. vm, err = newEngine() require.NoError(t, err, "unable to create engine") // This buffer will trace execution of the Script, dumping out to // stdout. var debugBuf bytes.Buffer done := false for !done { dis, err := vm.DisasmPC() require.NoError(t, err, "stepping") debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) done, err = vm.Step() if err != nil && valid { fmt.Println(debugBuf.String()) t.Fatalf("spend test case failed, spend "+ "should be valid: %v", err) } else if err == nil && !valid && done { fmt.Println(debugBuf.String()) t.Fatalf("spend test case succeed, spend "+ "should be invalid: %v", err) } debugBuf.WriteString( fmt.Sprintf("Stack: %v", vm.GetStack()), ) debugBuf.WriteString( fmt.Sprintf("AltStack: %v", vm.GetAltStack()), ) } // If we get to this point the unexpected case was not reached // during step execution, which happens for some checks, like // the clean-stack rule. validity := "invalid" if valid { validity = "valid" } fmt.Println(debugBuf.String()) t.Fatalf( "%v spend test case execution ended with: %v", validity, vmErr, ) } // TestHtlcV2 tests the HTLC V2 script success and timeout spend cases. func TestHtlcV2(t *testing.T) { const ( htlcValue = btcutil.Amount(1 * 10e8) testCltvExpiry = 24 ) 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 // doesn't need to exist, as we'll only be validating spending from the // transaction that references this. fundingOut := &wire.OutPoint{ Hash: chainhash.Hash(sha256.Sum256([]byte{1, 2, 3})), Index: 50, } fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil) sweepTx := wire.NewMsgTx(2) sweepTx.AddTxIn(fakeFundingTxIn) sweepTx.AddTxOut( &wire.TxOut{ PkScript: []byte("doesn't matter"), Value: int64(htlcValue), }, ) // Create sender and receiver keys. senderPrivKey, senderPubKey := test.CreateKey(1) receiverPrivKey, receiverPubKey := test.CreateKey(2) copy(receiverKey[:], receiverPubKey.SerializeCompressed()) copy(senderKey[:], senderPubKey.SerializeCompressed()) hash := sha256.Sum256(testPreimage[:]) // Create the htlc. htlc, err := NewHtlcV2( testCltvExpiry, senderKey, receiverKey, hash, &chaincfg.MainNetParams, ) require.NoError(t, err) // Create the htlc output we'll try to spend. htlcOutput := &wire.TxOut{ Value: int64(htlcValue), PkScript: htlc.PkScript, } // Create signers for sender and receiver. senderSigner := &input.MockSigner{ Privkeys: []*btcec.PrivateKey{senderPrivKey}, } 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, witnessScript []byte) ( input.Signature, error) { signDesc := &input.SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: pubkey, }, WitnessScript: witnessScript, Output: htlcOutput, HashType: htlc.SigHash(), SigHashes: txscript.NewTxSigHashes( tx, prevOutFetcher, ), InputIndex: 0, } return signer.SignOutputRaw(tx, signDesc) } 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 { sweepTx.TxIn[0].Sequence = htlc.SuccessSequence() sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, htlc.SuccessScript(), ) require.NoError(t, err) witness, err := htlc.GenSuccessWitness( sweepSig.Serialize(), testPreimage, ) 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 { sweepTx.TxIn[0].Sequence = 0 sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, htlc.SuccessScript(), ) require.NoError(t, err) witness, err := htlc.GenSuccessWitness( sweepSig.Serialize(), testPreimage, ) 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 { sweepTx.LockTime = testCltvExpiry - 1 sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, htlc.TimeoutScript(), ) require.NoError(t, err) witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) require.NoError(t, err) return witness }, false, }, { // Sender can spend after timeout. "timeout case spend after timeout", func(t *testing.T) wire.TxWitness { sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, htlc.TimeoutScript(), ) require.NoError(t, err) witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) require.NoError(t, err) return witness }, true, }, { // Receiver can't spend via timeout path. "timeout case receiver cannot spend", func(t *testing.T) wire.TxWitness { sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, receiverPubKey, receiverSigner, htlc.TimeoutScript(), ) require.NoError(t, err) witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) 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} // Create the htlc with the bogus key. htlc, err = NewHtlcV2( testCltvExpiry, bogusKey, receiverKey, hash, &chaincfg.MainNetParams, ) require.NoError(t, err) // Create the htlc output we'll try to spend. htlcOutput = &wire.TxOut{ Value: int64(htlcValue), PkScript: htlc.PkScript, } sweepTx.LockTime = testCltvExpiry sweepSig, err := signTx( sweepTx, senderPubKey, senderSigner, htlc.TimeoutScript(), ) require.NoError(t, err) witness, err := htlc.GenTimeoutWitness( sweepSig.Serialize(), ) require.NoError(t, err) return witness }, false, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { sweepTx.TxIn[0].Witness = testCase.witness(t) newEngine := func() (*txscript.Engine, error) { return txscript.NewEngine( htlc.PkScript, sweepTx, 0, txscript.StandardVerifyFlags, nil, nil, int64(htlcValue), prevOutFetcher, ) } assertEngineExecution(t, testCase.valid, newEngine) }) } } // TestHtlcV3 tests the HTLC V3 script success and timeout spend cases. func TestHtlcV3(t *testing.T) { versions := map[string]input.MuSig2Version{ "MuSig2 0.4": input.MuSig2Version040, "MuSig2 1.0RC2": input.MuSig2Version100RC2, } for name, version := range versions { t.Run(name, func(t *testing.T) { testHtlcV3(t, version) }) } } func testHtlcV3(t *testing.T, muSig2Version input.MuSig2Version) { var ( receiverKey [33]byte senderKey [33]byte ) preimage := [32]byte{1, 2, 3} p := lntypes.Preimage(preimage) hashedPreimage := sha256.Sum256(p[:]) value := int64(800_000) cltvExpiry := int32(10) senderPrivKey, senderPubKey := test.CreateKey(1) receiverPrivKey, receiverPubKey := test.CreateKey(2) copy(receiverKey[:], receiverPubKey.SerializeCompressed()) copy(senderKey[:], senderPubKey.SerializeCompressed()) htlc, err := NewHtlcV3( muSig2Version, cltvExpiry, senderKey, receiverKey, senderKey, receiverKey, hashedPreimage, &chaincfg.MainNetParams, ) require.NoError(t, err) 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, value, ) 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, htlc.SigHash(), 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) trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) require.True(t, ok) sig := signTx( tx, receiverPrivKey, txscript.NewBaseTapLeaf( trHtlc.SuccessScript(), ), ) 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 trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) require.True(t, ok) sig := signTx( tx, receiverPrivKey, txscript.NewBaseTapLeaf( trHtlc.SuccessScript(), ), ) 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 trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) require.True(t, ok) sig := signTx( tx, senderPrivKey, 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) trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) require.True(t, ok) sig := signTx( tx, senderPrivKey, txscript.NewBaseTapLeaf( trHtlc.TimeoutScript(), ), ) witness, err := htlc.GenTimeoutWitness(sig) require.NoError(t, err) return witness }, true, }, { // Receiver can't spend via timeout path. "timeout case receiver cannot spend", func(t *testing.T) wire.TxWitness { tx.TxIn[0].Sequence = htlc.SuccessSequence() tx.LockTime = uint32(cltvExpiry) trHtlc, ok := htlc.HtlcScript.(*HtlcScriptV3) require.True(t, ok) sig := signTx( tx, receiverPrivKey, 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 { var bogusKeyBytes [33]byte _, bogusKey := test.CreateKey(5) copy( bogusKeyBytes[:], bogusKey.SerializeCompressed(), ) htlc, err := NewHtlcV3( muSig2Version, cltvExpiry, senderKey, receiverKey, bogusKeyBytes, receiverKey, hashedPreimage, &chaincfg.MainNetParams, ) require.NoError(t, err) 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( senderPubKey, 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) }) } }