mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-24 14:50:40 +01:00
In this commit, we update all the taproot scripts to also accept an optional aux leaf. This aux leaf can be used to add more redemption paths for advanced channels, or just as an extra commitment space.
518 lines
15 KiB
Go
518 lines
15 KiB
Go
package lookout_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/btcutil/txsort"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
|
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
|
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
|
"github.com/lightningnetwork/lnd/watchtower/wtmock"
|
|
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const csvDelay uint32 = 144
|
|
|
|
var (
|
|
revPrivBytes = []byte{
|
|
0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f,
|
|
0x74, 0x6c, 0x9d, 0x5c, 0xae, 0x88, 0x2d, 0x31,
|
|
0x06, 0x90, 0xdd, 0x8c, 0x9b, 0x31, 0xbc, 0xd1,
|
|
0x78, 0x91, 0x88, 0x2a, 0xf9, 0x74, 0xa0, 0xef,
|
|
}
|
|
|
|
toLocalPrivBytes = []byte{
|
|
0xde, 0x17, 0xc1, 0x2f, 0xdc, 0x1b, 0xc0, 0xc6,
|
|
0x59, 0x5d, 0xf9, 0xc1, 0x3e, 0x89, 0xbc, 0x6f,
|
|
0x01, 0x85, 0x45, 0x76, 0x26, 0xce, 0x9c, 0x55,
|
|
0x3b, 0xc9, 0xec, 0x3d, 0xd8, 0x8b, 0xac, 0xa8,
|
|
}
|
|
|
|
toRemotePrivBytes = []byte{
|
|
0x28, 0x59, 0x6f, 0x36, 0xb8, 0x9f, 0x19, 0x5d,
|
|
0xcb, 0x07, 0x48, 0x8a, 0xe5, 0x89, 0x71, 0x74,
|
|
0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe,
|
|
0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f,
|
|
}
|
|
|
|
rewardCommitType = blob.TypeFromFlags(
|
|
blob.FlagReward, blob.FlagCommitOutputs,
|
|
)
|
|
|
|
altruistCommitType = blob.FlagCommitOutputs.Type()
|
|
|
|
altruistAnchorCommitType = blob.TypeAltruistAnchorCommit
|
|
|
|
altruisticTaprootCommitType = blob.TypeAltruistTaprootCommit
|
|
)
|
|
|
|
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
|
|
// correct justice transaction for different blob types.
|
|
func TestJusticeDescriptor(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
blobType blob.Type
|
|
}{
|
|
{
|
|
name: "reward and commit type",
|
|
blobType: rewardCommitType,
|
|
},
|
|
{
|
|
name: "altruist and commit type",
|
|
blobType: altruistCommitType,
|
|
},
|
|
{
|
|
name: "altruist anchor commit type",
|
|
blobType: altruistAnchorCommitType,
|
|
},
|
|
{
|
|
name: "altruist taproot commit type",
|
|
blobType: altruisticTaprootCommitType,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
testJusticeDescriptor(t, test.blobType)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
|
isAnchorChannel := blobType.IsAnchorChannel()
|
|
isTaprootChannel := blobType.IsTaprootChannel()
|
|
|
|
const (
|
|
localAmount = btcutil.Amount(100000)
|
|
remoteAmount = btcutil.Amount(200000)
|
|
totalAmount = localAmount + remoteAmount
|
|
)
|
|
|
|
// Parse the key pairs for all keys used in the test.
|
|
revSK, revPK := btcec.PrivKeyFromBytes(revPrivBytes)
|
|
_, toLocalPK := btcec.PrivKeyFromBytes(toLocalPrivBytes)
|
|
toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(toRemotePrivBytes)
|
|
|
|
// Get the commitment type.
|
|
commitType, err := blobType.CommitmentType(nil)
|
|
require.NoError(t, err)
|
|
|
|
// Create the signer, and add the revocation and to-remote privkeys.
|
|
signer := wtmock.NewMockSigner()
|
|
var (
|
|
revKeyLoc = signer.AddPrivKey(revSK)
|
|
toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK)
|
|
)
|
|
|
|
var (
|
|
toLocalScript, toLocalScriptHash []byte
|
|
toLocalCommitTree *input.CommitScriptTree
|
|
)
|
|
|
|
if isTaprootChannel {
|
|
toLocalCommitTree, err = input.NewLocalCommitScriptTree(
|
|
csvDelay, toLocalPK, revPK, fn.None[txscript.TapLeaf](),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
toLocalScript = toLocalCommitTree.RevocationLeaf.Script
|
|
|
|
toLocalScriptHash, err = input.PayToTaprootScript(
|
|
toLocalCommitTree.TaprootKey,
|
|
)
|
|
require.NoError(t, err)
|
|
} else {
|
|
// Construct the to-local witness script.
|
|
toLocalScript, err = input.CommitScriptToSelf(
|
|
csvDelay, toLocalPK, revPK,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Compute the to-local witness script hash.
|
|
toLocalScriptHash, err = input.WitnessScriptHash(toLocalScript)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Compute the to-remote redeem script, witness script hash, and
|
|
// sequence numbers.
|
|
//
|
|
// NOTE: This is pretty subtle.
|
|
//
|
|
// The actual redeem script for a p2wkh output is just the pubkey, but
|
|
// the witness sighash calculation injects the classic p2kh script:
|
|
// OP_DUP OP_HASH160 <pubkey-hash160> OP_EQUALVERIFY OP_CHECKSIG. When
|
|
// signing for p2wkh we don't pass the raw pubkey as the witness script
|
|
// to the sign descriptor (since that's also not a valid script).
|
|
// Instead we give it the _pkscript_ of the form OP_0 <pubkey-hash160>
|
|
// from which pubkey-hash160 is extracted during sighash calculation.
|
|
//
|
|
// On the other hand, signing for the anchor p2wsh to-remote outputs
|
|
// requires the sign descriptor to contain the redeem script ver batim.
|
|
// This difference in behavior forces us to use a distinct
|
|
// toRemoteSigningScript to handle both cases.
|
|
var (
|
|
toRemoteSequence uint32
|
|
toRemoteRedeemScript []byte
|
|
toRemoteScriptHash []byte
|
|
toRemoteSigningScript []byte
|
|
toRemoteCtrlBlock []byte
|
|
)
|
|
switch {
|
|
case isTaprootChannel:
|
|
toRemoteSequence = 1
|
|
|
|
commitScriptTree, err := input.NewRemoteCommitScriptTree(
|
|
toRemotePK, fn.None[txscript.TapLeaf](),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
toRemoteSigningScript = commitScriptTree.SettleLeaf.Script
|
|
|
|
toRemoteScriptHash, err = input.PayToTaprootScript(
|
|
commitScriptTree.TaprootKey,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
tree := commitScriptTree.TapscriptTree
|
|
settleTapleafHash := commitScriptTree.SettleLeaf.TapHash()
|
|
settleIdx := tree.LeafProofIndex[settleTapleafHash]
|
|
settleMerkleProof := tree.LeafMerkleProofs[settleIdx]
|
|
settleControlBlock := settleMerkleProof.ToControlBlock(
|
|
&input.TaprootNUMSKey,
|
|
)
|
|
|
|
ctrlBytes, err := settleControlBlock.ToBytes()
|
|
require.NoError(t, err)
|
|
toRemoteCtrlBlock = ctrlBytes
|
|
|
|
case isAnchorChannel:
|
|
toRemoteSequence = 1
|
|
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
|
|
toRemotePK,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
toRemoteScriptHash, err = input.WitnessScriptHash(
|
|
toRemoteRedeemScript,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// As it should be.
|
|
toRemoteSigningScript = toRemoteRedeemScript
|
|
|
|
default:
|
|
toRemoteRedeemScript = toRemotePK.SerializeCompressed()
|
|
toRemoteScriptHash, err = input.CommitScriptUnencumbered(
|
|
toRemotePK,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// NOTE: This is the _pkscript_.
|
|
toRemoteSigningScript = toRemoteScriptHash
|
|
}
|
|
|
|
// Construct the breaching commitment txn, containing the to-local and
|
|
// to-remote outputs. We don't need any inputs for this test.
|
|
breachTxn := &wire.MsgTx{
|
|
Version: 2,
|
|
TxIn: []*wire.TxIn{},
|
|
TxOut: []*wire.TxOut{
|
|
{
|
|
Value: int64(localAmount),
|
|
PkScript: toLocalScriptHash,
|
|
},
|
|
{
|
|
Value: int64(remoteAmount),
|
|
PkScript: toRemoteScriptHash,
|
|
},
|
|
},
|
|
}
|
|
breachTxID := breachTxn.TxHash()
|
|
|
|
// Compute the weight estimate for our justice transaction.
|
|
var weightEstimate input.TxWeightEstimator
|
|
|
|
// Add the local witness size to the weight estimator.
|
|
toLocalWitnessSize, err := commitType.ToLocalWitnessSize()
|
|
require.NoError(t, err)
|
|
weightEstimate.AddWitnessInput(toLocalWitnessSize)
|
|
|
|
// Add the remote witness size to the weight estimator.
|
|
toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize()
|
|
require.NoError(t, err)
|
|
weightEstimate.AddWitnessInput(toRemoteWitnessSize)
|
|
|
|
// Add the sweep output to the weight estimator.
|
|
weightEstimate.AddP2WKHOutput()
|
|
|
|
// Add the reward output to the weight estimator.
|
|
if blobType.Has(blob.FlagReward) {
|
|
weightEstimate.AddP2WKHOutput()
|
|
}
|
|
|
|
txWeight := weightEstimate.Weight()
|
|
|
|
// Create a session info so that simulate agreement of the sweep
|
|
// parameters that should be used in constructing the justice
|
|
// transaction.
|
|
policy := wtpolicy.Policy{
|
|
TxPolicy: wtpolicy.TxPolicy{
|
|
BlobType: blobType,
|
|
SweepFeeRate: 2000,
|
|
RewardRate: 900000,
|
|
},
|
|
}
|
|
sessionInfo := &wtdb.SessionInfo{
|
|
Policy: policy,
|
|
RewardAddress: makeAddrSlice(22),
|
|
}
|
|
|
|
breachInfo := &lnwallet.BreachRetribution{
|
|
RemoteDelay: csvDelay,
|
|
KeyRing: &lnwallet.CommitmentKeyRing{
|
|
ToLocalKey: toLocalPK,
|
|
ToRemoteKey: toRemotePK,
|
|
RevocationKey: revPK,
|
|
},
|
|
}
|
|
|
|
justiceKit, err := commitType.NewJusticeKit(
|
|
makeAddrSlice(22), breachInfo, true,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Create a transaction spending from the outputs of the breach
|
|
// transaction created earlier. The inputs are always ordered w/
|
|
// to-local and then to-remote. The outputs are always added as the
|
|
// sweep address then reward address.
|
|
justiceTxn := &wire.MsgTx{
|
|
Version: 2,
|
|
TxIn: []*wire.TxIn{
|
|
{
|
|
PreviousOutPoint: wire.OutPoint{
|
|
Hash: breachTxID,
|
|
Index: 0,
|
|
},
|
|
},
|
|
{
|
|
PreviousOutPoint: wire.OutPoint{
|
|
Hash: breachTxID,
|
|
Index: 1,
|
|
},
|
|
Sequence: toRemoteSequence,
|
|
},
|
|
},
|
|
}
|
|
|
|
outputs, err := policy.ComputeJusticeTxOuts(
|
|
totalAmount, txWeight, justiceKit.SweepAddress(),
|
|
sessionInfo.RewardAddress,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Attach the txouts and BIP69 sort the resulting transaction.
|
|
justiceTxn.TxOut = outputs
|
|
txsort.InPlaceSort(justiceTxn)
|
|
|
|
var (
|
|
toLocalSignDesc *input.SignDescriptor
|
|
toRemoteSignDesc *input.SignDescriptor
|
|
)
|
|
|
|
if isTaprootChannel {
|
|
prevOuts := map[wire.OutPoint]*wire.TxOut{
|
|
{
|
|
Hash: breachTxID,
|
|
Index: 0,
|
|
}: breachTxn.TxOut[0],
|
|
{
|
|
Hash: breachTxID,
|
|
Index: 1,
|
|
}: breachTxn.TxOut[1],
|
|
}
|
|
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
|
hashCache := txscript.NewTxSigHashes(
|
|
justiceTxn, prevOutputFetcher,
|
|
)
|
|
|
|
toLocalSignDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
KeyLocator: revKeyLoc,
|
|
},
|
|
WitnessScript: toLocalScript,
|
|
Output: breachTxn.TxOut[0],
|
|
SigHashes: hashCache,
|
|
PrevOutputFetcher: prevOutputFetcher,
|
|
InputIndex: 0,
|
|
HashType: txscript.SigHashDefault,
|
|
SignMethod: input.TaprootScriptSpendSignMethod,
|
|
}
|
|
|
|
toRemoteSignDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
KeyLocator: toRemoteKeyLoc,
|
|
PubKey: toRemotePK,
|
|
},
|
|
WitnessScript: toRemoteSigningScript,
|
|
Output: breachTxn.TxOut[1],
|
|
PrevOutputFetcher: prevOutputFetcher,
|
|
SigHashes: hashCache,
|
|
InputIndex: 1,
|
|
HashType: txscript.SigHashDefault,
|
|
SignMethod: input.TaprootScriptSpendSignMethod,
|
|
}
|
|
} else {
|
|
hashCache := input.NewTxSigHashesV0Only(justiceTxn)
|
|
|
|
// Create the sign descriptor used to sign for the to-local
|
|
// input.
|
|
toLocalSignDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
KeyLocator: revKeyLoc,
|
|
},
|
|
WitnessScript: toLocalScript,
|
|
Output: breachTxn.TxOut[0],
|
|
SigHashes: hashCache,
|
|
InputIndex: 0,
|
|
HashType: txscript.SigHashAll,
|
|
}
|
|
|
|
// Create the sign descriptor used to sign for the to-remote
|
|
// input.
|
|
toRemoteSignDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
KeyLocator: toRemoteKeyLoc,
|
|
PubKey: toRemotePK,
|
|
},
|
|
WitnessScript: toRemoteSigningScript,
|
|
Output: breachTxn.TxOut[1],
|
|
SigHashes: hashCache,
|
|
InputIndex: 1,
|
|
HashType: txscript.SigHashAll,
|
|
}
|
|
}
|
|
|
|
// Verify that our test justice transaction is sane.
|
|
btx := btcutil.NewTx(justiceTxn)
|
|
err = blockchain.CheckTransactionSanity(btx)
|
|
require.Nil(t, err)
|
|
|
|
// Compute a signature for the to-local input.
|
|
toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc)
|
|
require.Nil(t, err)
|
|
|
|
// Compute the witness for the to-remote input.
|
|
toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc)
|
|
require.Nil(t, err)
|
|
|
|
// Convert the to-local sig into a fixed-size signature.
|
|
toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw)
|
|
require.Nil(t, err)
|
|
|
|
// Convert the to-remote sig into a fixed-size signature.
|
|
toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw)
|
|
require.Nil(t, err)
|
|
|
|
// Complete our justice kit by copying the signatures into the payload.
|
|
justiceKit.AddToLocalSig(toLocalSig)
|
|
justiceKit.AddToRemoteSig(toRemoteSig)
|
|
|
|
justiceDesc := &lookout.JusticeDescriptor{
|
|
BreachedCommitTx: breachTxn,
|
|
SessionInfo: sessionInfo,
|
|
JusticeKit: justiceKit,
|
|
}
|
|
|
|
// Construct a breach punisher that will feed published transactions
|
|
// over the buffered channel.
|
|
publications := make(chan *wire.MsgTx, 1)
|
|
punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{
|
|
PublishTx: func(tx *wire.MsgTx, _ string) error {
|
|
publications <- tx
|
|
return nil
|
|
},
|
|
})
|
|
|
|
// Exact retribution on the offender. If no error is returned, we expect
|
|
// the justice transaction to be published via the channel.
|
|
err = punisher.Punish(justiceDesc, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Retrieve the published justice transaction.
|
|
var wtJusticeTxn *wire.MsgTx
|
|
select {
|
|
case wtJusticeTxn = <-publications:
|
|
case <-time.After(50 * time.Millisecond):
|
|
t.Fatalf("punisher did not publish justice txn")
|
|
}
|
|
|
|
if isTaprootChannel {
|
|
revokeLeaf := txscript.NewBaseTapLeaf(toLocalScript)
|
|
outputKey := txscript.ComputeTaprootOutputKey(
|
|
&input.TaprootNUMSKey, toLocalCommitTree.TapscriptRoot,
|
|
)
|
|
|
|
var outputKeyYIsOdd bool
|
|
if outputKey.SerializeCompressed()[0] ==
|
|
secp.PubKeyFormatCompressedOdd {
|
|
|
|
outputKeyYIsOdd = true
|
|
}
|
|
|
|
delayScriptHash := toLocalCommitTree.SettleLeaf.TapHash()
|
|
|
|
ctrlBlock := txscript.ControlBlock{
|
|
InternalKey: &input.TaprootNUMSKey,
|
|
OutputKeyYIsOdd: outputKeyYIsOdd,
|
|
LeafVersion: revokeLeaf.LeafVersion,
|
|
InclusionProof: delayScriptHash[:],
|
|
}
|
|
|
|
ctrlBytes, err := ctrlBlock.ToBytes()
|
|
require.NoError(t, err)
|
|
|
|
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
|
|
justiceTxn.TxIn[0].Witness[0] = toLocalSigRaw.Serialize()
|
|
justiceTxn.TxIn[0].Witness[1] = toLocalScript
|
|
justiceTxn.TxIn[0].Witness[2] = ctrlBytes
|
|
|
|
// Construct the test's to-remote witness.
|
|
justiceTxn.TxIn[1].Witness = make([][]byte, 3)
|
|
justiceTxn.TxIn[1].Witness[0] = toRemoteSigRaw.Serialize()
|
|
justiceTxn.TxIn[1].Witness[1] = toRemoteSigningScript
|
|
justiceTxn.TxIn[1].Witness[2] = toRemoteCtrlBlock
|
|
} else {
|
|
// Construct the test's to-local witness.
|
|
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
|
|
justiceTxn.TxIn[0].Witness[0] = append(
|
|
toLocalSigRaw.Serialize(),
|
|
byte(txscript.SigHashAll),
|
|
)
|
|
justiceTxn.TxIn[0].Witness[1] = []byte{1}
|
|
justiceTxn.TxIn[0].Witness[2] = toLocalScript
|
|
|
|
// Construct the test's to-remote witness.
|
|
justiceTxn.TxIn[1].Witness = make([][]byte, 2)
|
|
justiceTxn.TxIn[1].Witness[0] = append(
|
|
toRemoteSigRaw.Serialize(),
|
|
byte(txscript.SigHashAll),
|
|
)
|
|
justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript
|
|
}
|
|
|
|
// Assert that the watchtower derives the same justice txn.
|
|
require.Equal(t, justiceTxn, wtJusticeTxn)
|
|
}
|