349 lines
9.7 KiB
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))
|
||
|
}
|
||
|
}
|