lnd/input/script_utils_test.go
Oli 8047149c6a
multi: upgrade to btcd v2 modules
Migrate all btcd dependencies to the new per-package v2 modules (wire/v2,
txscript/v2, chaincfg/v2, chainhash/v2, btcutil/v2, psbt/v2, btcec/v2)
introduced by btcd v0.26.0, and pin the tagged ecosystem versions:
btcwallet v0.17.0, neutrino v0.18.0 and lightning-onion v1.4.0.

The bulk of the import rewrite was produced by the scripted diff from
https://github.com/btcsuite/btcd/pull/2547 (followed by 'make rpc'). The
address symbols that moved out of btcutil into the new address package
are imported as btcaddr where a local "address" variable would otherwise
shadow them. The go.mod/go.sum updates and the remaining manual
compilation fixes are folded into this single commit so it builds on its
own (the migration was previously split into a reproducible scripted-diff
plus follow-ups, intended to be squashed on merge).
2026-06-24 10:58:36 -07:00

2377 lines
68 KiB
Go

package input
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"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.
func assertEngineExecution(t *testing.T, testNum int, 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()
if valid == (vmErr == nil) {
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()
if err != nil {
t.Fatalf("stepping (%v)\n", err)
}
debugBuf.WriteString(fmt.Sprintf("Stepping %v\n", dis))
done, err = vm.Step()
if err != nil && valid {
t.Log(debugBuf.String())
t.Fatalf("spend test case #%v failed, spend "+
"should be valid: %v", testNum, err)
} else if err == nil && !valid && done {
t.Log(debugBuf.String())
t.Fatalf("spend test case #%v succeed, spend "+
"should be invalid: %v", testNum, err)
}
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
// during step execution, which happens for some checks, like
// the clean-stack rule.
validity := "invalid"
if valid {
validity = "valid"
}
t.Log(debugBuf.String())
t.Fatalf("%v spend test case #%v execution ended with: %v", validity, testNum, vmErr)
}
// TestRevocationKeyDerivation tests that given a public key, and a revocation
// hash, the homomorphic revocation public and private key derivation work
// properly.
func TestRevocationKeyDerivation(t *testing.T) {
t.Parallel()
// First, we'll generate a commitment point, and a commitment secret.
// These will be used to derive the ultimate revocation keys.
revocationPreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revocationPreimage)
// With the commitment secrets generated, we'll now create the base
// keys we'll use to derive the revocation key from.
basePriv, basePub := btcec.PrivKeyFromBytes(testWalletPrivKey)
// With the point and key obtained, we can now derive the revocation
// key itself.
revocationPub := DeriveRevocationPubkey(basePub, commitPoint)
// The revocation public key derived from the original public key, and
// the one derived from the private key should be identical.
revocationPriv := DeriveRevocationPrivKey(basePriv, commitSecret)
if !revocationPub.IsEqual(revocationPriv.PubKey()) {
t.Fatalf("derived public keys don't match!")
}
}
// TestTweakKeyDerivation tests that given a public key, and commitment tweak,
// then we're able to properly derive a tweaked private key that corresponds to
// the computed tweak public key. This scenario ensure that our key derivation
// for any of the non revocation keys on the commitment transaction is correct.
func TestTweakKeyDerivation(t *testing.T) {
t.Parallel()
// First, we'll generate a base public key that we'll be "tweaking".
baseSecret := testHdSeed.CloneBytes()
basePriv, basePub := btcec.PrivKeyFromBytes(baseSecret)
// With the base key create, we'll now create a commitment point, and
// from that derive the bytes we'll used to tweak the base public key.
commitPoint := ComputeCommitmentPoint(bobsPrivKey)
commitTweak := SingleTweakBytes(commitPoint, basePub)
// Next, we'll modify the public key. When we apply the same operation
// to the private key we should get a key that matches.
tweakedPub := TweakPubKey(basePub, commitPoint)
// Finally, attempt to re-generate the private key that matches the
// tweaked public key. The derived key should match exactly.
derivedPriv := TweakPrivKey(basePriv, commitTweak)
if !derivedPriv.PubKey().IsEqual(tweakedPub) {
t.Fatalf("pub keys don't match")
}
}
// makeWitnessTestCase is a helper function used within test cases involving
// the validity of a crafted witness. This function is a wrapper function which
// allows constructing table-driven tests. In the case of an error while
// constructing the witness, the test fails fatally.
func makeWitnessTestCase(t *testing.T,
f func() (wire.TxWitness, error)) func() wire.TxWitness {
return func() wire.TxWitness {
witness, err := f()
if err != nil {
t.Fatalf("unable to create witness test case: %v", err)
}
return witness
}
}
// TestHTLCSenderSpendValidation tests all possible valid+invalid redemption
// paths in the script used within the sender's commitment transaction for an
// outgoing HTLC.
//
// The following cases are exercised by this test:
// sender script:
// - receiver spends
// - revoke w/ sig
// - HTLC with invalid preimage size
// - HTLC with valid preimage size + sig
// - sender spends
// - invalid lock-time for CLTV
// - invalid sequence for CSV
// - valid lock-time+sequence, valid sig
func TestHTLCSenderSpendValidation(t *testing.T) {
t.Parallel()
// 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.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err, "unable to create txid")
fundingOut := &wire.OutPoint{
Hash: *txid,
Index: 50,
}
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
// Next we'll the commitment secret for our commitment tx and also the
// revocation key that we'll use as well.
revokePreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revokePreimage)
// Generate a payment preimage to be used below.
paymentPreimage := revokePreimage
paymentPreimage[0] ^= 1
paymentHash := sha256.Sum256(paymentPreimage[:])
// We'll also need some tests keys for alice and bob, and metadata of
// the HTLC output.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(bobsPrivKey)
paymentAmt := btcutil.Amount(1 * 10e8)
aliceLocalKey := TweakPubKey(aliceKeyPub, commitPoint)
bobLocalKey := TweakPubKey(bobKeyPub, commitPoint)
// As we'll be modeling spends from Alice's commitment transaction,
// we'll be using Bob's base point for the revocation key.
revocationKey := DeriveRevocationPubkey(bobKeyPub, commitPoint)
bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub)
aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub)
// Finally, we'll create mock signers for both of them based on their
// private keys. This test simplifies a bit and uses the same key as
// the base point for all scripts and derivations.
bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
var (
htlcWitnessScript, htlcPkScript []byte
htlcOutput *wire.TxOut
sweepTxSigHashes *txscript.TxSigHashes
senderCommitTx, sweepTx *wire.MsgTx
bobRecvrSig *ecdsa.Signature
bobSigHash txscript.SigHashType
)
// genCommitTx generates a commitment tx where the htlc output requires
// confirmation to be spent according to 'confirmed'.
genCommitTx := func(confirmed bool) {
// Generate the raw HTLC redemption scripts, and its p2wsh
// counterpart.
htlcWitnessScript, err = SenderHTLCScript(
aliceLocalKey, bobLocalKey, revocationKey,
paymentHash[:], confirmed,
)
if err != nil {
t.Fatalf("unable to create htlc sender script: %v", err)
}
htlcPkScript, err = WitnessScriptHash(htlcWitnessScript)
if err != nil {
t.Fatalf("unable to create p2wsh htlc script: %v", err)
}
// This will be Alice's commitment transaction. In this
// scenario Alice is sending an HTLC to a node she has a path
// to (could be Bob, could be multiple hops down, it doesn't
// really matter).
htlcOutput = &wire.TxOut{
Value: int64(paymentAmt),
PkScript: htlcPkScript,
}
senderCommitTx = wire.NewMsgTx(2)
senderCommitTx.AddTxIn(fakeFundingTxIn)
senderCommitTx.AddTxOut(htlcOutput)
}
// genSweepTx generates a sweep of the senderCommitTx, and sets the
// sequence and sighash single|anyonecanspend if confirmed is true.
genSweepTx := func(confirmed bool) {
prevOut := &wire.OutPoint{
Hash: senderCommitTx.TxHash(),
Index: 0,
}
sweepTx = wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil))
if confirmed {
sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1)
}
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
sweepTxSigHashes = NewTxSigHashesV0Only(sweepTx)
bobSigHash = txscript.SigHashAll
if confirmed {
bobSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay
}
// We'll also generate a signature on the sweep transaction above
// that will act as Bob's signature to Alice for the second level HTLC
// transaction.
bobSignDesc := SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: bobSigHash,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
bobSig, err := bobSigner.SignOutputRaw(sweepTx, &bobSignDesc)
if err != nil {
t.Fatalf("unable to generate alice signature: %v", err)
}
bobRecvrSig, err = ecdsa.ParseDERSignature(bobSig.Serialize())
if err != nil {
t.Fatalf("unable to parse signature: %v", err)
}
}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// revoke w/ sig
// TODO(roasbeef): test invalid revoke
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendRevokeWithKey(bobSigner, signDesc,
revocationKey, sweepTx)
}),
true,
},
{
// HTLC with invalid preimage size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendRedeem(bobSigner, signDesc,
sweepTx,
// Invalid preimage length
bytes.Repeat([]byte{1}, 45))
}),
false,
},
{
// HTLC with valid preimage size + sig
// TODO(roasbeef): invalid preimage
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendRedeem(bobSigner, signDesc,
sweepTx, paymentPreimage)
}),
true,
},
{
// HTLC with valid preimage size + sig, and with
// enforced locktime in HTLC script.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Make a commit tx that needs confirmation for
// HTLC output to be spent.
genCommitTx(true)
// Generate a sweep with the locktime set.
genSweepTx(true)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendRedeem(bobSigner, signDesc,
sweepTx, paymentPreimage)
}),
true,
},
{
// HTLC with valid preimage size + sig, but trying to
// spend CSV output without sequence set.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Generate commitment tx with 1 CSV locked
// HTLC.
genCommitTx(true)
// Generate sweep tx that doesn't have locktime
// enabled.
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendRedeem(bobSigner, signDesc,
sweepTx, paymentPreimage)
}),
false,
},
{
// valid spend to the transition the state of the HTLC
// output with the second level HTLC timeout
// transaction.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendTimeout(
bobRecvrSig, bobSigHash, aliceSigner,
signDesc, sweepTx,
)
}),
true,
},
{
// valid spend to the transition the state of the HTLC
// output with the second level HTLC timeout
// transaction.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Make a commit tx that needs confirmation for
// HTLC output to be spent.
genCommitTx(true)
// Generate a sweep with the locktime set.
genSweepTx(true)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendTimeout(
bobRecvrSig, bobSigHash, aliceSigner,
signDesc, sweepTx,
)
}),
true,
},
{
// valid spend to the transition the state of the HTLC
// output with the second level HTLC timeout
// transaction.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Generate commitment tx with 1 CSV locked
// HTLC.
genCommitTx(true)
// Generate sweep tx that doesn't have locktime
// enabled.
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return SenderHtlcSpendTimeout(
bobRecvrSig, bobSigHash, aliceSigner,
signDesc, sweepTx,
)
}),
false,
},
}
// TODO(roasbeef): set of cases to ensure able to sign w/ keypath and
// not
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
htlcPkScript, sweepTx, 0,
txscript.StandardVerifyFlags, nil, nil,
int64(paymentAmt),
txscript.NewCannedPrevOutputFetcher(
htlcPkScript, int64(paymentAmt),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestHTLCReceiverSpendValidation tests all possible valid+invalid redemption
// paths in the script used within the receiver's commitment transaction for an
// incoming HTLC.
//
// The following cases are exercised by this test:
// - receiver spends
// 1. HTLC redemption w/ invalid preimage size
// 2. HTLC redemption w/ invalid sequence
// 3. HTLC redemption w/ valid preimage size
// - sender spends
// 1. revoke w/ sig
// 2. refund w/ invalid lock time
// 3. refund w/ valid lock time
func TestHTLCReceiverSpendValidation(t *testing.T) {
t.Parallel()
// 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.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err, "unable to create txid")
fundingOut := &wire.OutPoint{
Hash: *txid,
Index: 50,
}
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
// Next we'll the commitment secret for our commitment tx and also the
// revocation key that we'll use as well.
revokePreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revokePreimage)
// Generate a payment preimage to be used below.
paymentPreimage := revokePreimage
paymentPreimage[0] ^= 1
paymentHash := sha256.Sum256(paymentPreimage[:])
// We'll also need some tests keys for alice and bob, and metadata of
// the HTLC output.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(bobsPrivKey)
paymentAmt := btcutil.Amount(1 * 10e8)
cltvTimeout := uint32(8)
aliceLocalKey := TweakPubKey(aliceKeyPub, commitPoint)
bobLocalKey := TweakPubKey(bobKeyPub, commitPoint)
// As we'll be modeling spends from Bob's commitment transaction, we'll
// be using Alice's base point for the revocation key.
revocationKey := DeriveRevocationPubkey(aliceKeyPub, commitPoint)
bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub)
aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub)
// Finally, we'll create mock signers for both of them based on their
// private keys. This test simplifies a bit and uses the same key as
// the base point for all scripts and derivations.
bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
var (
htlcWitnessScript, htlcPkScript []byte
htlcOutput *wire.TxOut
receiverCommitTx, sweepTx *wire.MsgTx
sweepTxSigHashes *txscript.TxSigHashes
aliceSenderSig *ecdsa.Signature
aliceSigHash txscript.SigHashType
)
genCommitTx := func(confirmed bool) {
// Generate the raw HTLC redemption scripts, and its p2wsh
// counterpart.
htlcWitnessScript, err = ReceiverHTLCScript(
cltvTimeout, aliceLocalKey, bobLocalKey, revocationKey,
paymentHash[:], confirmed,
)
if err != nil {
t.Fatalf("unable to create htlc sender script: %v", err)
}
htlcPkScript, err = WitnessScriptHash(htlcWitnessScript)
if err != nil {
t.Fatalf("unable to create p2wsh htlc script: %v", err)
}
// This will be Bob's commitment transaction. In this scenario Alice is
// sending an HTLC to a node she has a path to (could be Bob, could be
// multiple hops down, it doesn't really matter).
htlcOutput = &wire.TxOut{
Value: int64(paymentAmt),
PkScript: htlcWitnessScript,
}
receiverCommitTx = wire.NewMsgTx(2)
receiverCommitTx.AddTxIn(fakeFundingTxIn)
receiverCommitTx.AddTxOut(htlcOutput)
}
genSweepTx := func(confirmed bool) {
prevOut := &wire.OutPoint{
Hash: receiverCommitTx.TxHash(),
Index: 0,
}
sweepTx = wire.NewMsgTx(2)
sweepTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *prevOut,
})
if confirmed {
sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1)
}
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
sweepTxSigHashes = NewTxSigHashesV0Only(sweepTx)
aliceSigHash = txscript.SigHashAll
if confirmed {
aliceSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay
}
// We'll also generate a signature on the sweep transaction above
// that will act as Alice's signature to Bob for the second level HTLC
// transaction.
aliceSignDesc := SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: aliceSigHash,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
aliceSig, err := aliceSigner.SignOutputRaw(sweepTx, &aliceSignDesc)
if err != nil {
t.Fatalf("unable to generate alice signature: %v", err)
}
aliceSenderSig, err = ecdsa.ParseDERSignature(
aliceSig.Serialize(),
)
if err != nil {
t.Fatalf("unable to parse signature: %v", err)
}
}
// TODO(roasbeef): modify valid to check precise script errors?
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// HTLC redemption w/ invalid preimage size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendRedeem(
aliceSenderSig, aliceSigHash,
bytes.Repeat([]byte{1}, 45), bobSigner,
signDesc, sweepTx,
)
}),
false,
},
{
// HTLC redemption w/ valid preimage size
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendRedeem(
aliceSenderSig, aliceSigHash,
paymentPreimage, bobSigner,
signDesc, sweepTx,
)
}),
true,
},
{
// revoke w/ sig
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendRevokeWithKey(aliceSigner,
signDesc, revocationKey, sweepTx)
}),
true,
},
{
// HTLC redemption w/ valid preimage size, and with
// enforced locktime in HTLC scripts.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Make a commit tx that needs confirmation for
// HTLC output to be spent.
genCommitTx(true)
// Generate a sweep with the locktime set.
genSweepTx(true)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendRedeem(
aliceSenderSig, aliceSigHash,
paymentPreimage, bobSigner,
signDesc, sweepTx,
)
}),
true,
},
{
// HTLC redemption w/ valid preimage size, but trying
// to spend CSV output without sequence set.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Generate commitment tx with 1 CSV locked
// HTLC.
genCommitTx(true)
// Generate sweep tx that doesn't have locktime
// enabled.
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: bobCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendRedeem(
aliceSenderSig, aliceSigHash,
paymentPreimage, bobSigner, signDesc,
sweepTx,
)
}),
false,
},
{
// refund w/ invalid lock time
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendTimeout(aliceSigner, signDesc,
sweepTx, int32(cltvTimeout-2))
}),
false,
},
{
// refund w/ valid lock time
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
genCommitTx(false)
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendTimeout(aliceSigner, signDesc,
sweepTx, int32(cltvTimeout))
}),
true,
},
{
// refund w/ valid lock time, and enforced locktime in
// HTLC scripts.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Make a commit tx that needs confirmation for
// HTLC output to be spent.
genCommitTx(true)
// Generate a sweep with the locktime set.
genSweepTx(true)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendTimeout(aliceSigner, signDesc,
sweepTx, int32(cltvTimeout))
}),
true,
},
{
// refund w/ valid lock time, but no sequence set in
// sweep tx trying to spend CSV locked HTLC output.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
// Generate commitment tx with 1 CSV locked
// HTLC.
genCommitTx(true)
// Generate sweep tx that doesn't have locktime
// enabled.
genSweepTx(false)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
SingleTweak: aliceCommitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return ReceiverHtlcSpendTimeout(aliceSigner, signDesc,
sweepTx, int32(cltvTimeout))
}),
false,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
htlcPkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(paymentAmt),
txscript.NewCannedPrevOutputFetcher(
htlcPkScript, int64(paymentAmt),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestSecondLevelHtlcSpends tests all the possible redemption clauses from the
// HTLC success and timeout covenant transactions.
func TestSecondLevelHtlcSpends(t *testing.T) {
t.Parallel()
// We'll start be creating a creating a 2BTC HTLC.
const htlcAmt = btcutil.Amount(2 * 10e8)
// In all of our scenarios, the CSV timeout to claim a self output will
// be 5 blocks.
const claimDelay = 5
// First we'll set up some initial key state for Alice and Bob that
// will be used in the scripts we created below.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(bobsPrivKey)
revokePreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revokePreimage)
// As we're modeling this as Bob sweeping the HTLC on-chain from his
// commitment transaction after a period of time, we'll be using a
// revocation key derived from Alice's base point and his secret.
revocationKey := DeriveRevocationPubkey(aliceKeyPub, commitPoint)
// Next, craft a fake HTLC outpoint that we'll use to generate the
// sweeping transaction using.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err, "unable to create txid")
htlcOutPoint := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(htlcOutPoint, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
// The delay key will be crafted using Bob's public key as the output
// we created will be spending from Alice's commitment transaction.
delayKey := TweakPubKey(bobKeyPub, commitPoint)
// The commit tweak will be required in order for Bob to derive the
// proper key need to spend the output.
commitTweak := SingleTweakBytes(commitPoint, bobKeyPub)
// Finally we'll generate the HTLC script itself that we'll be spending
// from. The revocation clause can be claimed by Alice, while Bob can
// sweep the output after a particular delay.
htlcWitnessScript, err := SecondLevelHtlcScript(revocationKey,
delayKey, claimDelay)
require.NoError(t, err, "unable to create htlc script")
htlcPkScript, err := WitnessScriptHash(htlcWitnessScript)
require.NoError(t, err, "unable to create htlc output")
htlcOutput := &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlcAmt),
}
// TODO(roasbeef): make actually use timeout/success txns?
// Finally, we'll create mock signers for both of them based on their
// private keys. This test simplifies a bit and uses the same key as
// the base point for all scripts and derivations.
bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Sender of the HTLC attempts to activate the
// revocation clause, but uses the wrong key (fails to
// use the double tweak in this case).
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendRevoke(aliceSigner, signDesc,
sweepTx)
}),
false,
},
{
// Sender of HTLC activates the revocation clause.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendRevoke(aliceSigner, signDesc,
sweepTx)
}),
true,
},
{
// Receiver of the HTLC attempts to sweep, but tries to
// do so pre-maturely with a smaller CSV delay (2
// blocks instead of 5 blocks).
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: commitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(bobSigner, signDesc,
sweepTx, claimDelay-3)
}),
false,
},
{
// Receiver of the HTLC sweeps with the proper CSV
// delay, but uses the wrong key (leaves off the single
// tweak).
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(bobSigner, signDesc,
sweepTx, claimDelay)
}),
false,
},
{
// Receiver of the HTLC sweeps with the proper CSV
// delay, and the correct key.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: commitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(bobSigner, signDesc,
sweepTx, claimDelay)
}),
true,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
htlcPkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(htlcAmt),
txscript.NewCannedPrevOutputFetcher(
htlcPkScript, int64(htlcAmt),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestLeaseSecondLevelHtlcSpends tests all the possible redemption clauses from
// the HTLC success and timeout covenant transactions in script enforced lease
// commitments.
func TestLeaseSecondLevelHtlcSpends(t *testing.T) {
t.Parallel()
// We'll start be creating a creating a 2BTC HTLC.
const htlcAmt = btcutil.Amount(2 * 10e8)
// In all of our scenarios, the CSV timeout to claim a self output will
// be 5 blocks.
const claimDelay = 5
// In all of our scenarios, the CLTV timelock will expire at height
// 1337.
const leaseExpiry = 1337
// First we'll set up some initial key state for Alice and Bob that
// will be used in the scripts we created below.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(bobsPrivKey)
revokePreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revokePreimage)
// As we're modeling this as Bob sweeping the HTLC on-chain from his
// commitment transaction after a period of time, we'll be using a
// revocation key derived from Alice's base point and his secret.
revocationKey := DeriveRevocationPubkey(aliceKeyPub, commitPoint)
// Next, craft a fake HTLC outpoint that we'll use to generate the
// sweeping transaction using.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err)
htlcOutPoint := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(htlcOutPoint, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
// The delay key will be crafted using Bob's public key as the output
// we created will be spending from Alice's commitment transaction.
delayKey := TweakPubKey(bobKeyPub, commitPoint)
// The commit tweak will be required in order for Bob to derive the
// proper key need to spend the output.
commitTweak := SingleTweakBytes(commitPoint, bobKeyPub)
// Finally we'll generate the HTLC script itself that we'll be spending
// from. The revocation clause can be claimed by Alice, while Bob can
// sweep the output after a particular delay.
htlcWitnessScript, err := LeaseSecondLevelHtlcScript(
revocationKey, delayKey, claimDelay, leaseExpiry,
)
require.NoError(t, err)
htlcPkScript, err := WitnessScriptHash(htlcWitnessScript)
require.NoError(t, err)
htlcOutput := &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlcAmt),
}
// Finally, we'll create mock signers for both of them based on their
// private keys. This test simplifies a bit and uses the same key as
// the base point for all scripts and derivations.
bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Sender of the HTLC attempts to activate the
// revocation clause, but uses the wrong key (fails to
// use the double tweak in this case).
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendRevoke(
aliceSigner, signDesc, sweepTx,
)
}),
false,
},
{
// Sender of HTLC activates the revocation clause.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendRevoke(
aliceSigner, signDesc, sweepTx,
)
}),
true,
},
{
// Receiver of the HTLC attempts to sweep, but tries to
// do so pre-maturely with a smaller CSV delay (2
// blocks instead of 5 blocks), even after the CLTV
// timelock expires.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: commitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(
bobSigner, signDesc, sweepTx, claimDelay-3,
)
}),
false,
},
{
// Receiver of the HTLC sweeps with the proper CSV delay
// and after the CLTV timelock expires, but uses the
// wrong key (leaves off the single tweak).
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(
bobSigner, signDesc, sweepTx, claimDelay,
)
}),
false,
},
{
// Receiver of the HTLC sweeps with the proper CSV
// delay, and the correct key, but before the CTLV
// timelock expires.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = 0
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: commitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(
bobSigner, signDesc, sweepTx, claimDelay,
)
}),
false,
},
{
// Receiver of the HTLC sweeps with the proper CSV
// delay, and the correct key after the CTLV timelock
// expires.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
SingleTweak: commitTweak,
WitnessScript: htlcWitnessScript,
Output: htlcOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return HtlcSpendSuccess(
bobSigner, signDesc, sweepTx, claimDelay,
)
}),
true,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
htlcPkScript, sweepTx, 0,
txscript.StandardVerifyFlags, nil, nil,
int64(htlcAmt),
txscript.NewCannedPrevOutputFetcher(
htlcPkScript, int64(htlcAmt),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestLeaseCommmitSpendToSelf tests all the possible redemption clauses from
// the to_self output in a script enforced lease commitment transaction.
func TestLeaseCommmitSpendToSelf(t *testing.T) {
t.Parallel()
const (
outputVal = btcutil.Amount(2 * 10e8)
csvDelay = 5
leaseExpiry = 1337
)
// Set up some initial key state for Alice and Bob that will be used in
// the scripts we created below.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(bobsPrivKey)
// We'll have Bob take the revocation path in some cases.
revokePreimage := testHdSeed.CloneBytes()
commitSecret, commitPoint := btcec.PrivKeyFromBytes(revokePreimage)
revocationKey := DeriveRevocationPubkey(bobKeyPub, commitPoint)
// Construct the script enforced lease to_self commitment transaction
// output.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err)
commitOut := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
commitScript, err := LeaseCommitScriptToSelf(
aliceKeyPub, revocationKey, csvDelay, leaseExpiry,
)
require.NoError(t, err)
commitPkScript, err := WitnessScriptHash(commitScript)
require.NoError(t, err)
commitOutput := &wire.TxOut{
PkScript: commitPkScript,
Value: int64(outputVal),
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(commitOut, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
// Create mock signers for both parties to ensure signatures are
// produced and verified correctly.
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Bob can spend with his revocation key, but not
// without the proper tweak.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendRevoke(
bobSigner, signDesc, sweepTx,
)
}),
false,
},
{
// Bob can spend with his revocation key with the proper
// tweak.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: bobKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendRevoke(
bobSigner, signDesc, sweepTx,
)
}),
true,
},
{
// Alice cannot spend with the proper key before the CSV
// delay and after the CLTV timelock has expired.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
sweepTx.TxIn[0].Sequence = LockTimeToSequence(
false, csvDelay/2,
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendTimeout(
aliceSigner, signDesc, sweepTx,
)
}),
false,
},
{
// Alice cannot spend with the proper key after the CSV
// delay and before the CLTV timelock has expired.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = 0
sweepTx.TxIn[0].Sequence = LockTimeToSequence(
false, csvDelay,
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
DoubleTweak: commitSecret,
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendTimeout(
aliceSigner, signDesc, sweepTx,
)
}),
false,
},
{
// Alice can spend with the proper key after the CSV
// delay and CLTV timelock have expired.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
sweepTx.TxIn[0].Sequence = LockTimeToSequence(
false, csvDelay,
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendTimeout(
aliceSigner, signDesc, sweepTx,
)
}),
true,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
commitPkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(outputVal),
txscript.NewCannedPrevOutputFetcher(
commitPkScript, int64(outputVal),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestCommitSpendToRemoteConfirmed checks that the delayed version of the
// to_remote version can only be spent by the owner, and after one
// confirmation.
func TestCommitSpendToRemoteConfirmed(t *testing.T) {
t.Parallel()
const outputVal = btcutil.Amount(2 * 10e8)
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err, "unable to create txid")
commitOut := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
commitScript, err := CommitScriptToRemoteConfirmed(aliceKeyPub)
require.NoError(t, err, "unable to create htlc script")
commitPkScript, err := WitnessScriptHash(commitScript)
require.NoError(t, err, "unable to create htlc output")
commitOutput := &wire.TxOut{
PkScript: commitPkScript,
Value: int64(outputVal),
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(commitOut, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Alice can spend after the a CSV delay has passed.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendToRemoteConfirmed(aliceSigner, signDesc,
sweepTx)
}),
true,
},
{
// Alice cannot spend output without sequence set.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendToRemoteConfirmed(aliceSigner, signDesc,
sweepTx)
}),
false,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
commitPkScript, sweepTx, 0,
txscript.StandardVerifyFlags, nil, nil,
int64(outputVal),
txscript.NewCannedPrevOutputFetcher(
commitPkScript, int64(outputVal),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestLeaseCommitSpendToRemoteConfirmed checks that the delayed version of the
// to_remote version can only be spent by the owner, after one confirmation, and
// after the lease expiration has been met.
func TestLeaseCommitSpendToRemoteConfirmed(t *testing.T) {
t.Parallel()
const (
outputVal = btcutil.Amount(2 * 10e8)
leaseExpiry = 1337
)
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(
testWalletPrivKey,
)
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err)
commitOut := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
commitScript, err := LeaseCommitScriptToRemoteConfirmed(
aliceKeyPub, leaseExpiry,
)
require.NoError(t, err)
commitPkScript, err := WitnessScriptHash(commitScript)
require.NoError(t, err)
commitOutput := &wire.TxOut{
PkScript: commitPkScript,
Value: int64(outputVal),
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(commitOut, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Alice can spend after the CSV delay and CLTV timelock
// has passed.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
sweepTx.TxIn[0].Sequence = LockTimeToSequence(
false, 1,
)
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendToRemoteConfirmed(
aliceSigner, signDesc, sweepTx,
)
}),
true,
},
{
// Alice cannot spend output without sequence set, even
// once the CLTV timelock has expired.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = leaseExpiry
sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendToRemoteConfirmed(
aliceSigner, signDesc, sweepTx,
)
}),
false,
},
{
// Alice cannot spend output without sequence or
// locktime set.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.LockTime = 0
sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: commitScript,
Output: commitOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendToRemoteConfirmed(
aliceSigner, signDesc, sweepTx,
)
}),
false,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
commitPkScript, sweepTx, 0,
txscript.StandardVerifyFlags, nil, nil,
int64(outputVal),
txscript.NewCannedPrevOutputFetcher(
commitPkScript, int64(outputVal),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestSpendAnchor checks that we can spend the anchors using the various spend
// paths.
func TestSpendAnchor(t *testing.T) {
t.Parallel()
const anchorSize = 294
// First we'll set up some initial key state for Alice.
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(testWalletPrivKey)
// Create a fake anchor outpoint that we'll use to generate the
// sweeping transaction.
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
require.NoError(t, err, "unable to create txid")
anchorOutPoint := &wire.OutPoint{
Hash: *txid,
Index: 0,
}
sweepTx := wire.NewMsgTx(2)
sweepTx.AddTxIn(wire.NewTxIn(anchorOutPoint, nil, nil))
sweepTx.AddTxOut(
&wire.TxOut{
PkScript: []byte("doesn't matter"),
Value: 1 * 10e8,
},
)
// Generate the anchor script that can be spent by Alice immediately,
// or by anyone after 16 blocks.
anchorScript, err := CommitScriptAnchor(aliceKeyPub)
require.NoError(t, err, "unable to create htlc script")
anchorPkScript, err := WitnessScriptHash(anchorScript)
require.NoError(t, err, "unable to create htlc output")
anchorOutput := &wire.TxOut{
PkScript: anchorPkScript,
Value: int64(anchorSize),
}
// Create mock signer for Alice.
aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}}
testCases := []struct {
witness func() wire.TxWitness
valid bool
}{
{
// Alice can spend immediately.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum
sweepTxSigHashes := NewTxSigHashesV0Only(sweepTx)
signDesc := &SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
PubKey: aliceKeyPub,
},
WitnessScript: anchorScript,
Output: anchorOutput,
HashType: txscript.SigHashAll,
SigHashes: sweepTxSigHashes,
InputIndex: 0,
}
return CommitSpendAnchor(aliceSigner, signDesc,
sweepTx)
}),
true,
},
{
// Anyone can spend after 16 blocks.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 16)
return CommitSpendAnchorAnyone(anchorScript)
}),
true,
},
{
// Anyone cannot spend before 16 blocks.
makeWitnessTestCase(t, func() (wire.TxWitness, error) {
sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 15)
return CommitSpendAnchorAnyone(anchorScript)
}),
false,
},
}
for i, testCase := range testCases {
sweepTx.TxIn[0].Witness = testCase.witness()
newEngine := func() (*txscript.Engine, error) {
return txscript.NewEngine(
anchorPkScript,
sweepTx, 0, txscript.StandardVerifyFlags, nil,
nil, int64(anchorSize),
txscript.NewCannedPrevOutputFetcher(
anchorPkScript, int64(anchorSize),
),
)
}
assertEngineExecution(t, i, testCase.valid, newEngine)
}
}
// TestSpecificationKeyDerivation implements the test vectors provided in
// BOLT-03, Appendix E.
func TestSpecificationKeyDerivation(t *testing.T) {
const (
baseSecretHex = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
perCommitmentSecretHex = "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
basePointHex = "036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"
perCommitmentPointHex = "025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486"
)
baseSecret, err := privkeyFromHex(baseSecretHex)
require.NoError(t, err, "Failed to parse serialized privkey")
perCommitmentSecret, err := privkeyFromHex(perCommitmentSecretHex)
require.NoError(t, err, "Failed to parse serialized privkey")
basePoint, err := pubkeyFromHex(basePointHex)
require.NoError(t, err, "Failed to parse serialized pubkey")
perCommitmentPoint, err := pubkeyFromHex(perCommitmentPointHex)
require.NoError(t, err, "Failed to parse serialized pubkey")
// name: derivation of key from basepoint and per_commitment_point
const expectedLocalKeyHex = "0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5"
actualLocalKey := TweakPubKey(basePoint, perCommitmentPoint)
actualLocalKeyHex := pubkeyToHex(actualLocalKey)
if actualLocalKeyHex != expectedLocalKeyHex {
t.Errorf("Incorrect derivation of local public key: "+
"expected %v, got %v", expectedLocalKeyHex, actualLocalKeyHex)
}
// name: derivation of secret key from basepoint secret and per_commitment_secret
const expectedLocalPrivKeyHex = "cbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f"
tweak := SingleTweakBytes(perCommitmentPoint, basePoint)
actualLocalPrivKey := TweakPrivKey(baseSecret, tweak)
actualLocalPrivKeyHex := privkeyToHex(actualLocalPrivKey)
if actualLocalPrivKeyHex != expectedLocalPrivKeyHex {
t.Errorf("Incorrect derivation of local private key: "+
"expected %v, got %v, %v", expectedLocalPrivKeyHex,
actualLocalPrivKeyHex, hex.EncodeToString(tweak))
}
// name: derivation of revocation key from basepoint and per_commitment_point
const expectedRevocationKeyHex = "02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0"
actualRevocationKey := DeriveRevocationPubkey(basePoint, perCommitmentPoint)
actualRevocationKeyHex := pubkeyToHex(actualRevocationKey)
if actualRevocationKeyHex != expectedRevocationKeyHex {
t.Errorf("Incorrect derivation of revocation public key: "+
"expected %v, got %v", expectedRevocationKeyHex,
actualRevocationKeyHex)
}
// name: derivation of revocation secret from basepoint_secret and per_commitment_secret
const expectedRevocationPrivKeyHex = "d09ffff62ddb2297ab000cc85bcb4283fdeb6aa052affbc9dddcf33b61078110"
actualRevocationPrivKey := DeriveRevocationPrivKey(baseSecret,
perCommitmentSecret)
actualRevocationPrivKeyHex := privkeyToHex(actualRevocationPrivKey)
if actualRevocationPrivKeyHex != expectedRevocationPrivKeyHex {
t.Errorf("Incorrect derivation of revocation private key: "+
"expected %v, got %v", expectedRevocationPrivKeyHex,
actualRevocationPrivKeyHex)
}
}
// BenchmarkScriptBuilderAlloc benchmarks the script builder's default
// allocation.
func BenchmarkScriptBuilderAlloc(t *testing.B) {
dummyData := []byte("dummy data")
randomPriv, err := btcec.NewPrivateKey()
require.NoError(t, err)
randomPub := randomPriv.PubKey()
randomPubBytes := randomPub.SerializeCompressed()
t.ReportAllocs()
t.ResetTimer()
// We run each iteration 1000 times to make the script allocation stand
// out a bit more vs. the overhead of the rest of the function calls.
for i := 0; i < t.N*1000; i++ {
err := runScriptAllocTest(dummyData, randomPubBytes, randomPub)
if err != nil {
t.Fatal(err)
}
}
}
// TestScriptBuilderAlloc makes sure the scripts generated by the utils have the
// correct length.
func TestScriptBuilderAlloc(t *testing.T) {
t.Parallel()
dummyData := []byte("dummy data")
randomPriv, err := btcec.NewPrivateKey()
require.NoError(t, err)
randomPub := randomPriv.PubKey()
randomPubBytes := randomPub.SerializeCompressed()
err = runScriptAllocTest(dummyData, randomPubBytes, randomPub)
require.NoError(t, err)
}
// runScriptAllocTest runs the script size test. We don't use the "require"
// library to assert the length to not influence the benchmark too much.
func runScriptAllocTest(dummyData, randomPubBytes []byte,
randomPub *btcec.PublicKey) error {
const (
maxCLTV = 500000000
maxCSV = (1 << 31) - 1
)
script, err := WitnessScriptHash(dummyData)
if err != nil {
return err
}
if len(script) != P2WSHSize {
return fmt.Errorf("expected script size of %d", P2WSHSize)
}
script, err = WitnessPubKeyHash(randomPubBytes)
if err != nil {
return err
}
if len(script) != P2WPKHSize {
return fmt.Errorf("expected script size of %d", P2WPKHSize)
}
script, err = GenerateP2SH(dummyData)
if err != nil {
return err
}
if len(script) != NestedP2WPKHSize {
return fmt.Errorf("expected script size of %d",
NestedP2WPKHSize)
}
script, err = GenerateP2PKH(dummyData)
if err != nil {
return err
}
if len(script) != P2PKHSize {
return fmt.Errorf("expected script size of %d", P2PKHSize)
}
script, err = GenMultiSigScript(randomPubBytes, randomPubBytes)
if err != nil {
return err
}
if len(script) != MultiSigSize {
return fmt.Errorf("expected script size of %d", MultiSigSize)
}
script, err = SenderHTLCScript(
randomPub, randomPub, randomPub, dummyData, false,
)
if err != nil {
return err
}
if len(script) != OfferedHtlcScriptSize {
return fmt.Errorf("expected script size of %d",
OfferedHtlcScriptSize)
}
script, err = SenderHTLCScript(
randomPub, randomPub, randomPub, dummyData, true,
)
if err != nil {
return err
}
if len(script) != OfferedHtlcScriptSizeConfirmed {
return fmt.Errorf("expected script size of %d",
OfferedHtlcScriptSizeConfirmed)
}
script, err = ReceiverHTLCScript(
maxCLTV, randomPub, randomPub, randomPub, dummyData, false,
)
if err != nil {
return err
}
if len(script) != AcceptedHtlcScriptSize {
return fmt.Errorf("expected script size of %d",
AcceptedHtlcScriptSize)
}
script, err = ReceiverHTLCScript(
maxCLTV, randomPub, randomPub, randomPub, dummyData, true,
)
if err != nil {
return err
}
if len(script) != AcceptedHtlcScriptSizeConfirmed {
return fmt.Errorf("expected script size of %d",
AcceptedHtlcScriptSizeConfirmed)
}
script, err = SecondLevelHtlcScript(randomPub, randomPub, maxCSV)
if err != nil {
return err
}
if len(script) != ToLocalScriptSize {
return fmt.Errorf("expected script size of %d",
ToLocalScriptSize)
}
script, err = LeaseSecondLevelHtlcScript(
randomPub, randomPub, maxCSV, maxCLTV,
)
if err != nil {
return err
}
if len(script) != ToLocalScriptSize+LeaseWitnessScriptSizeOverhead {
return fmt.Errorf("expected script size of %d",
ToLocalScriptSize+LeaseWitnessScriptSizeOverhead)
}
script, err = CommitScriptToSelf(maxCSV, randomPub, randomPub)
if err != nil {
return err
}
if len(script) != ToLocalScriptSize {
return fmt.Errorf("expected script size of %d",
ToLocalScriptSize)
}
script, err = LeaseCommitScriptToSelf(
randomPub, randomPub, maxCSV, maxCLTV,
)
if err != nil {
return err
}
if len(script) != ToLocalScriptSize+LeaseWitnessScriptSizeOverhead {
return fmt.Errorf("expected script size of %d",
ToLocalScriptSize+LeaseWitnessScriptSizeOverhead)
}
script, err = CommitScriptUnencumbered(randomPub)
if err != nil {
return err
}
if len(script) != P2WPKHSize {
return fmt.Errorf("expected script size of %d", P2WPKHSize)
}
script, err = CommitScriptToRemoteConfirmed(randomPub)
if err != nil {
return err
}
if len(script) != ToRemoteConfirmedScriptSize {
return fmt.Errorf("expected script size of %d",
ToRemoteConfirmedScriptSize)
}
script, err = LeaseCommitScriptToRemoteConfirmed(randomPub, maxCSV)
if err != nil {
return err
}
if len(script) !=
ToRemoteConfirmedScriptSize+LeaseWitnessScriptSizeOverhead {
return fmt.Errorf("expected script size of %d",
ToRemoteConfirmedScriptSize+
LeaseWitnessScriptSizeOverhead)
}
script, err = CommitScriptAnchor(randomPub)
if err != nil {
return err
}
if len(script) != AnchorScriptSize {
return fmt.Errorf("expected script size of %d",
AnchorScriptSize)
}
return nil
}
// TestTaprootHtlcScriptGeneration tests that taproot HTLC scripts can be
// generated with both staging and production script options.
func TestTaprootHtlcScriptGeneration(t *testing.T) {
t.Parallel()
// Generate test keys.
senderKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
senderPubKey := senderKey.PubKey()
receiverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
receiverPubKey := receiverKey.PubKey()
revokeKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
revokePubKey := revokeKey.PubKey()
// Test constants.
cltvExpiry := uint32(500000)
hashBytes := make([]byte, 32)
copy(hashBytes, []byte("test payment hash"))
// Use empty auxiliary leaf and local commit.
auxLeaf := NoneTapLeaf()
whoseCommit := lntypes.Local
// Test SenderHTLCScriptTaproot with staging vs production scripts.
stagingSenderScript, err := SenderHTLCScriptTaproot(
senderPubKey, receiverPubKey, revokePubKey, hashBytes,
whoseCommit, auxLeaf,
)
require.NoError(t, err)
prodSenderScript, err := SenderHTLCScriptTaproot(
senderPubKey, receiverPubKey, revokePubKey, hashBytes,
whoseCommit, auxLeaf, WithProdScripts(),
)
require.NoError(t, err)
// Verify that both script trees are generated successfully.
require.NotNil(t,
stagingSenderScript, "staging sender script should "+
"be generated",
)
require.NotNil(t,
prodSenderScript, "production sender script should "+
"be generated",
)
// Test ReceiverHTLCScriptTaproot with staging vs production scripts.
stagingReceiverScript, err := ReceiverHTLCScriptTaproot(
cltvExpiry, senderPubKey, receiverPubKey, revokePubKey,
hashBytes,
whoseCommit, auxLeaf,
)
require.NoError(t, err)
prodReceiverScript, err := ReceiverHTLCScriptTaproot(
cltvExpiry, senderPubKey, receiverPubKey, revokePubKey,
hashBytes,
whoseCommit, auxLeaf, WithProdScripts(),
)
require.NoError(t, err)
// Verify that both script trees are generated successfully.
require.NotNil(t, stagingReceiverScript,
"staging receiver script should be generated")
require.NotNil(t, prodReceiverScript,
"production receiver script should be generated")
// Scripts should be different between staging and production.
// Note: The sender success script (redeemed by receiver)
// should differ.
require.NotEqual(t,
stagingSenderScript.SuccessTapLeaf.Script,
prodSenderScript.SuccessTapLeaf.Script,
"staging and production sender success scripts should differ",
)
require.NotEqual(t, stagingReceiverScript.TimeoutTapLeaf.Script,
prodReceiverScript.TimeoutTapLeaf.Script,
"staging and production receiver timeout scripts should differ")
// Production scripts should be smaller due to
// OP_CHECKSIGVERIFY optimizations.
require.Less(t, len(prodSenderScript.SuccessTapLeaf.Script),
len(stagingSenderScript.SuccessTapLeaf.Script),
"production sender success script should be smaller "+
"than staging",
)
require.Less(t, len(prodReceiverScript.TimeoutTapLeaf.Script),
len(stagingReceiverScript.TimeoutTapLeaf.Script),
"production receiver timeout script should be smaller "+
"than staging",
)
// Verify the script trees have non-empty script bytes. Using
// require.NotNil on TapLeaf value types is always true, so we
// check Script bytes directly.
require.NotEmpty(t, stagingSenderScript.TimeoutTapLeaf.Script,
"staging sender timeout leaf should have script bytes")
require.NotEmpty(t, prodSenderScript.TimeoutTapLeaf.Script,
"production sender timeout leaf should have script bytes")
require.NotEmpty(t, stagingReceiverScript.SuccessTapLeaf.Script,
"staging receiver success leaf should have script bytes")
require.NotEmpty(t, prodReceiverScript.SuccessTapLeaf.Script,
"production receiver success leaf should have script bytes")
// The timeout leaf for sender HTLC is unchanged between staging
// and production (SenderHTLCTapLeafTimeout ignores script opts).
require.Equal(t,
stagingSenderScript.TimeoutTapLeaf.Script,
prodSenderScript.TimeoutTapLeaf.Script,
"sender timeout leaf should be identical across variants",
)
// The success leaf for receiver HTLC is unchanged between staging
// and production (ReceiverHtlcTapLeafSuccess ignores script opts).
require.Equal(t,
stagingReceiverScript.SuccessTapLeaf.Script,
prodReceiverScript.SuccessTapLeaf.Script,
"receiver success leaf should be identical across variants",
)
}