mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 10:39:01 +01:00
518 lines
14 KiB
Go
518 lines
14 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/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,
|
|
)
|
|
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,
|
|
)
|
|
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, int64(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)
|
|
}
|