lnd.xprv/watchtower/lookout/justice_descriptor_test.go

349 lines
9.7 KiB
Go

// +build dev
package lookout_test
import (
"reflect"
"testing"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"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"
)
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,
}
)
type mockSigner struct {
index uint32
keys map[keychain.KeyLocator]*btcec.PrivateKey
}
func newMockSigner() *mockSigner {
return &mockSigner{
keys: make(map[keychain.KeyLocator]*btcec.PrivateKey),
}
}
func (s *mockSigner) SignOutputRaw(tx *wire.MsgTx,
signDesc *lnwallet.SignDescriptor) ([]byte, error) {
witnessScript := signDesc.WitnessScript
amt := signDesc.Output.Value
privKey, ok := s.keys[signDesc.KeyDesc.KeyLocator]
if !ok {
panic("cannot sign w/ unknown key")
}
sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
witnessScript, signDesc.HashType, privKey,
)
if err != nil {
return nil, err
}
return sig[:len(sig)-1], nil
}
func (s *mockSigner) ComputeInputScript(tx *wire.MsgTx,
signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) {
return nil, nil
}
func (s *mockSigner) addPrivKey(privKey *btcec.PrivateKey) keychain.KeyLocator {
keyLoc := keychain.KeyLocator{
Index: s.index,
}
s.index++
s.keys[keyLoc] = privKey
return keyLoc
}
func TestJusticeDescriptor(t *testing.T) {
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(
btcec.S256(), revPrivBytes,
)
_, toLocalPK := btcec.PrivKeyFromBytes(
btcec.S256(), toLocalPrivBytes,
)
toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(
btcec.S256(), toRemotePrivBytes,
)
// Create the signer, and add the revocation and to-remote privkeys.
signer := newMockSigner()
var (
revKeyLoc = signer.addPrivKey(revSK)
toRemoteKeyLoc = signer.addPrivKey(toRemoteSK)
)
// Construct the to-local witness script.
toLocalScript, err := lnwallet.CommitScriptToSelf(
csvDelay, toLocalPK, revPK,
)
if err != nil {
t.Fatalf("unable to create to-local script: %v", err)
}
// Compute the to-local witness script hash.
toLocalScriptHash, err := lnwallet.WitnessScriptHash(toLocalScript)
if err != nil {
t.Fatalf("unable to create to-local witness script hash: %v", err)
}
// Compute the to-remote witness script hash.
toRemoteScriptHash, err := lnwallet.CommitScriptUnencumbered(toRemotePK)
if err != nil {
t.Fatalf("unable to create to-remote script: %v", err)
}
// 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 lnwallet.TxWeightEstimator
weightEstimate.AddP2WKHOutput()
weightEstimate.AddP2WKHOutput()
weightEstimate.AddWitnessInput(lnwallet.ToLocalPenaltyWitnessSize)
weightEstimate.AddWitnessInput(lnwallet.P2WKHWitnessSize)
txWeight := weightEstimate.Weight()
// Create a session info so that simulate agreement of the sweep
// parameters that should be used in constructing the justice
// transaction.
sessionInfo := &wtdb.SessionInfo{
SweepFeeRate: 2000,
RewardRate: 900000,
RewardAddress: makeAddrSlice(22),
}
// Given the total input amount and the weight estimate, compute the
// amount that should be swept for the victim and the amount taken as a
// reward by the watchtower.
sweepAmt, rewardAmt, err := sessionInfo.ComputeSweepOutputs(
totalAmount, int64(txWeight),
)
if err != nil {
t.Fatalf("unable to compute sweep outputs: %v", err)
}
// Begin to assemble the justice kit, starting with the sweep address,
// pubkeys, and csv delay.
justiceKit := &blob.JusticeKit{
SweepAddress: makeAddrSlice(22),
CSVDelay: csvDelay,
}
copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed())
copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed())
copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed())
// 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,
},
},
},
TxOut: []*wire.TxOut{
{
Value: int64(sweepAmt),
PkScript: justiceKit.SweepAddress,
},
{
Value: int64(rewardAmt),
PkScript: sessionInfo.RewardAddress,
},
},
}
hashCache := txscript.NewTxSigHashes(justiceTxn)
// Create the sign descriptor used to sign for the to-local input.
toLocalSignDesc := &lnwallet.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 := &lnwallet.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: toRemoteKeyLoc,
PubKey: toRemotePK,
},
WitnessScript: toRemoteScriptHash,
Output: breachTxn.TxOut[1],
SigHashes: hashCache,
InputIndex: 1,
HashType: txscript.SigHashAll,
}
// Verify that our test justice transaction is sane.
btx := btcutil.NewTx(justiceTxn)
if err := blockchain.CheckTransactionSanity(btx); err != nil {
t.Fatalf("justice txn is not sane: %v", err)
}
// Compute a DER-encoded signature for the to-local input.
toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc)
if err != nil {
t.Fatalf("unable to sign to-local input: %v", err)
}
// Compute the witness for the to-remote input. The first element is a
// DER-encoded signature under the to-remote pubkey. The sighash flag is
// also present, so we trim it.
toRemoteWitness, err := lnwallet.CommitSpendNoDelay(
signer, toRemoteSignDesc, justiceTxn,
)
if err != nil {
t.Fatalf("unable to sign to-remote input: %v", err)
}
toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1]
// Convert the DER to-local sig into a fixed-size signature.
toLocalSig, err := lnwire.NewSigFromRawSignature(toLocalSigRaw)
if err != nil {
t.Fatalf("unable to parse to-local signature: %v", err)
}
// Convert the DER to-remote sig into a fixed-size signature.
toRemoteSig, err := lnwire.NewSigFromRawSignature(toRemoteSigRaw)
if err != nil {
t.Fatalf("unable to parse to-remote signature: %v", err)
}
// Complete our justice kit by copying the signatures into the payload.
copy(justiceKit.CommitToLocalSig[:], toLocalSig[:])
copy(justiceKit.CommitToRemoteSig[:], 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) 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)
if err != nil {
t.Fatalf("unable to punish breach: %v", 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")
}
// Construct the test's to-local witness.
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
justiceTxn.TxIn[0].Witness[0] = append(toLocalSigRaw,
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,
byte(txscript.SigHashAll))
justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed()
// Assert that the watchtower derives the same justice txn.
if !reflect.DeepEqual(justiceTxn, wtJusticeTxn) {
t.Fatalf("expected justice txn: %v\ngot %v",
spew.Sdump(justiceTxn),
spew.Sdump(wtJusticeTxn))
}
}