Merge pull request #2516 from cfromknecht/wtclient-backup-task

watchtower: client justice transaction creation + blob encryption
This commit is contained in:
Olaoluwa Osuntokun 2019-02-05 21:38:10 -08:00 committed by GitHub
commit 9cd88a04b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 913 additions and 0 deletions

@ -95,6 +95,18 @@ func MakeBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
}
}
// NewBaseInput allocates and assembles a new *BaseInput that can be used to
// construct a sweep transaction.
func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
signDescriptor *SignDescriptor, heightHint uint32) *BaseInput {
input := MakeBaseInput(
outpoint, witnessType, signDescriptor, heightHint,
)
return &input
}
// CraftInputScript returns a valid set of input scripts allowing this output
// to be spent. The returns input scripts should target the input at location
// txIndex within the passed transaction. The input scripts generated by this

@ -0,0 +1,296 @@
package wtclient
import (
"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/btcsuite/btcutil/txsort"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtdb"
)
// backupTask is an internal struct for computing the justice transaction for a
// particular revoked state. A backupTask functions as a scratch pad for storing
// computing values of the transaction itself, such as the final split in
// balance if the justice transaction will give a reward to the tower. The
// backup task has three primary phases:
// 1. Init: Determines which inputs from the breach transaction will be spent,
// and the total amount contained in the inputs.
// 2. Bind: Asserts that the revoked state is eligible under a given session's
// parameters. Certain states may be ineligible due to fee rates, too little
// input amount, etc. Backup of these states can be deferred to a later time
// or session with more favorable parameters. If the session is bound
// successfully, the final session-dependent values to the justice
// transaction are solidified.
// 3. Send: Once the task is bound, it will be queued to send to a specific
// tower corresponding to the session in which it was bound. The justice
// transaction will be assembled by examining the parameters left as a
// result of the binding. After the justice transaction is signed, the
// necessary components are stripped out and encrypted before being sent to
// the tower in a StateUpdate.
type backupTask struct {
chanID lnwire.ChannelID
commitHeight uint64
breachInfo *lnwallet.BreachRetribution
// state-dependent variables
toLocalInput input.Input
toRemoteInput input.Input
totalAmt btcutil.Amount
// session-dependent variables
blobType blob.Type
outputs []*wire.TxOut
}
// newBackupTask initializes a new backupTask and populates all state-dependent
// variables.
func newBackupTask(chanID *lnwire.ChannelID,
breachInfo *lnwallet.BreachRetribution) *backupTask {
// Parse the non-dust outputs from the breach transaction,
// simultaneously computing the total amount contained in the inputs
// present. We can't compute the exact output values at this time
// since the task has not been assigned to a session, at which point
// parameters such as fee rate, number of outputs, and reward rate will
// be finalized.
var (
totalAmt int64
toLocalInput input.Input
toRemoteInput input.Input
)
// Add the sign descriptors and outputs corresponding to the to-local
// and to-remote outputs, respectively, if either input amount is
// non-dust. Note that the naming here seems reversed, but both are
// correct. For example, the to-remote output on the remote party's
// commitment is an output that pays to us. Hence the retribution refers
// to that output as local, though relative to their commitment, it is
// paying to-the-remote party (which is us).
if breachInfo.RemoteOutputSignDesc != nil {
toLocalInput = input.NewBaseInput(
&breachInfo.RemoteOutpoint,
input.CommitmentRevoke,
breachInfo.RemoteOutputSignDesc,
0,
)
totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
}
if breachInfo.LocalOutputSignDesc != nil {
toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint,
input.CommitmentNoDelay,
breachInfo.LocalOutputSignDesc,
0,
)
totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
}
return &backupTask{
chanID: *chanID,
commitHeight: breachInfo.RevokedStateNum,
breachInfo: breachInfo,
toLocalInput: toLocalInput,
toRemoteInput: toRemoteInput,
totalAmt: btcutil.Amount(totalAmt),
}
}
// inputs returns all non-dust inputs that we will attempt to spend from.
//
// NOTE: Ordering of the inputs is not critical as we sort the transaction with
// BIP69.
func (t *backupTask) inputs() map[wire.OutPoint]input.Input {
inputs := make(map[wire.OutPoint]input.Input)
if t.toLocalInput != nil {
inputs[*t.toLocalInput.OutPoint()] = t.toLocalInput
}
if t.toRemoteInput != nil {
inputs[*t.toRemoteInput.OutPoint()] = t.toRemoteInput
}
return inputs
}
// bindSession determines if the backupTask is compatible with the passed
// SessionInfo's policy. If no error is returned, the task has been bound to the
// session and can be queued to upload to the tower. Otherwise, the bind failed
// and should be rescheduled with a different session.
func (t *backupTask) bindSession(session *wtdb.SessionInfo,
sweepPkScript []byte) error {
// First we'll begin by deriving a weight estimate for the justice
// transaction. The final weight can be different depending on whether
// the watchtower is taking a reward.
var weightEstimate input.TxWeightEstimator
// All justice transactions have a p2wkh output paying to the victim.
weightEstimate.AddP2WKHOutput()
// Next, add the contribution from the inputs that are present on this
// breach transaction.
if t.toLocalInput != nil {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
}
if t.toRemoteInput != nil {
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
}
// Now, compute the output values depending on whether FlagReward is set
// in the current session's policy.
outputs, err := session.Policy.ComputeJusticeTxOuts(
t.totalAmt, int64(weightEstimate.Weight()),
sweepPkScript, session.RewardAddress,
)
if err != nil {
return err
}
t.outputs = outputs
t.blobType = session.Policy.BlobType
return nil
}
// craftSessionPayload is the final stage for a backupTask, and generates the
// encrypted payload and breach hint that should be sent to the tower. This
// method computes the final justice transaction using the bound
// session-dependent variables, and signs the resulting transaction. The
// required pieces from signatures, witness scripts, etc are then packaged into
// a JusticeKit and encrypted using the breach transaction's key.
func (t *backupTask) craftSessionPayload(sweepPkScript []byte,
signer input.Signer) (wtdb.BreachHint, []byte, error) {
var hint wtdb.BreachHint
// First, copy over the sweep pkscript, the pubkeys used to derive the
// to-local script, and the remote CSV delay.
keyRing := t.breachInfo.KeyRing
justiceKit := &blob.JusticeKit{
SweepAddress: sweepPkScript,
RevocationPubKey: toBlobPubKey(keyRing.RevocationKey),
LocalDelayPubKey: toBlobPubKey(keyRing.DelayKey),
CSVDelay: t.breachInfo.RemoteDelay,
}
// If this commitment has an output that pays to us, copy the to-remote
// pubkey into the justice kit. This serves as the indicator to the
// tower that we expect the breaching transaction to have a non-dust
// output to spend from.
if t.toRemoteInput != nil {
justiceKit.CommitToRemotePubKey = toBlobPubKey(
keyRing.NoDelayKey,
)
}
// Now, begin construction of the justice transaction. We'll start with
// a version 2 transaction.
justiceTxn := wire.NewMsgTx(2)
// Next, add the non-dust inputs that were derived from the breach
// information. This will either be contain both the to-local and
// to-remote outputs, or only be the to-local output.
inputs := t.inputs()
for prevOutPoint := range inputs {
justiceTxn.AddTxIn(&wire.TxIn{
PreviousOutPoint: prevOutPoint,
})
}
// Add the sweep output paying directly to the user and possibly a
// reward output, using the outputs computed when the task was bound.
justiceTxn.TxOut = t.outputs
// Sort the justice transaction according to BIP69.
txsort.InPlaceSort(justiceTxn)
// Check that the justice transaction meets basic validity requirements
// before attempting to attach the witnesses.
btx := btcutil.NewTx(justiceTxn)
if err := blockchain.CheckTransactionSanity(btx); err != nil {
return hint, nil, err
}
// Construct a sighash cache to improve signing performance.
hashCache := txscript.NewTxSigHashes(justiceTxn)
// Since the transaction inputs could have been reordered as a result of
// the BIP69 sort, create an index mapping each prevout to it's new
// index.
inputIndex := make(map[wire.OutPoint]int)
for i, txIn := range justiceTxn.TxIn {
inputIndex[txIn.PreviousOutPoint] = i
}
// Now, iterate through the list of inputs that were initially added to
// the transaction and store the computed witness within the justice
// kit.
for _, inp := range inputs {
// Lookup the input's new post-sort position.
i := inputIndex[*inp.OutPoint()]
// Construct the full witness required to spend this input.
inputScript, err := inp.CraftInputScript(
signer, justiceTxn, hashCache, i,
)
if err != nil {
return hint, nil, err
}
// Parse the DER-encoded signature from the first position of
// the resulting witness. We trim an extra byte to remove the
// sighash flag.
witness := inputScript.Witness
rawSignature := witness[0][:len(witness[0])-1]
// Reencode the DER signature into a fixed-size 64 byte
// signature.
signature, err := lnwire.NewSigFromRawSignature(rawSignature)
if err != nil {
return hint, nil, err
}
// Finally, copy the serialized signature into the justice kit,
// using the input's witness type to select the appropriate
// field.
switch inp.WitnessType() {
case input.CommitmentRevoke:
copy(justiceKit.CommitToLocalSig[:], signature[:])
case input.CommitmentNoDelay:
copy(justiceKit.CommitToRemoteSig[:], signature[:])
}
}
// Compute the breach hint from the breach transaction id's prefix.
breachKey := t.breachInfo.BreachTransaction.TxHash()
// Then, we'll encrypt the computed justice kit using the full breach
// transaction id, which will allow the tower to recover the contents
// after the transaction is seen in the chain or mempool.
encBlob, err := justiceKit.Encrypt(breachKey[:], t.blobType)
if err != nil {
return hint, nil, err
}
// Finally, compute the breach hint, taken as the first half of the
// breach transactions txid. Once the tower sees the breach transaction
// on the network, it can use the full txid to decyrpt the blob.
hint = wtdb.NewBreachHintFromHash(&breachKey)
return hint, encBlob, nil
}
// toBlobPubKey serializes the given pubkey into a blob.PubKey that can be set
// as a field on a blob.JusticeKit.
func toBlobPubKey(pubKey *btcec.PublicKey) blob.PubKey {
var blobPubKey blob.PubKey
copy(blobPubKey[:], pubKey.SerializeCompressed())
return blobPubKey
}

@ -0,0 +1,605 @@
package wtclient
import (
"bytes"
"crypto/rand"
"io"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"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/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtmock"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
)
const csvDelay uint32 = 144
var (
zeroPK [33]byte
zeroSig [64]byte
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,
}
)
func makeAddrSlice(size int) []byte {
addr := make([]byte, size)
if _, err := io.ReadFull(rand.Reader, addr); err != nil {
panic("cannot make addr")
}
return addr
}
type backupTaskTest struct {
name string
chanID lnwire.ChannelID
breachInfo *lnwallet.BreachRetribution
expToLocalInput input.Input
expToRemoteInput input.Input
expTotalAmt btcutil.Amount
expSweepAmt int64
expRewardAmt int64
expRewardScript []byte
session *wtdb.SessionInfo
bindErr error
expSweepScript []byte
signer input.Signer
}
// genTaskTest creates a instance of a backupTaskTest using the passed
// parameters. This method handles generating a breach transaction and its
// corresponding BreachInfo, as well as setting the wtpolicy.Policy of the given
// session.
func genTaskTest(
name string,
stateNum uint64,
toLocalAmt int64,
toRemoteAmt int64,
blobType blob.Type,
sweepFeeRate lnwallet.SatPerKWeight,
rewardScript []byte,
expSweepAmt int64,
expRewardAmt int64,
bindErr error) backupTaskTest {
// 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 := wtmock.NewMockSigner()
var (
revKeyLoc = signer.AddPrivKey(revSK)
toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK)
)
// First, we'll initialize a new breach transaction and the
// corresponding breach retribution. The retribution stores a pointer to
// the breach transaction, which we will continue to modify.
breachTxn := wire.NewMsgTx(2)
breachInfo := &lnwallet.BreachRetribution{
RevokedStateNum: stateNum,
BreachTransaction: breachTxn,
KeyRing: &lnwallet.CommitmentKeyRing{
RevocationKey: revPK,
DelayKey: toLocalPK,
NoDelayKey: toRemotePK,
},
RemoteDelay: csvDelay,
}
// Add the sign descriptors and outputs corresponding to the to-local
// and to-remote outputs, respectively, if either input amount is
// non-zero. Note that the naming here seems reversed, but both are
// correct. For example, the to-remote output on the remote party's
// commitment is an output that pays to us. Hence the retribution refers
// to that output as local, though relative to their commitment, it is
// paying to-the-remote party (which is us).
if toLocalAmt > 0 {
toLocalSignDesc := &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: revKeyLoc,
PubKey: revPK,
},
Output: &wire.TxOut{
Value: toLocalAmt,
},
HashType: txscript.SigHashAll,
}
breachInfo.RemoteOutputSignDesc = toLocalSignDesc
breachTxn.AddTxOut(toLocalSignDesc.Output)
}
if toRemoteAmt > 0 {
toRemoteSignDesc := &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: toRemoteKeyLoc,
PubKey: toRemotePK,
},
Output: &wire.TxOut{
Value: toRemoteAmt,
},
HashType: txscript.SigHashAll,
}
breachInfo.LocalOutputSignDesc = toRemoteSignDesc
breachTxn.AddTxOut(toRemoteSignDesc.Output)
}
var (
toLocalInput input.Input
toRemoteInput input.Input
)
// Now that the breach transaction has all its outputs, we can compute
// its txid and inputs spending from it. We also generate the
// input.Inputs that should be derived by the backup task.
txid := breachTxn.TxHash()
var index uint32
if toLocalAmt > 0 {
breachInfo.RemoteOutpoint = wire.OutPoint{
Hash: txid,
Index: index,
}
toLocalInput = input.NewBaseInput(
&breachInfo.RemoteOutpoint,
input.CommitmentRevoke,
breachInfo.RemoteOutputSignDesc,
0,
)
index++
}
if toRemoteAmt > 0 {
breachInfo.LocalOutpoint = wire.OutPoint{
Hash: txid,
Index: index,
}
toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint,
input.CommitmentNoDelay,
breachInfo.LocalOutputSignDesc,
0,
)
}
return backupTaskTest{
name: name,
breachInfo: breachInfo,
expToLocalInput: toLocalInput,
expToRemoteInput: toRemoteInput,
expTotalAmt: btcutil.Amount(toLocalAmt + toRemoteAmt),
expSweepAmt: expSweepAmt,
expRewardAmt: expRewardAmt,
expRewardScript: rewardScript,
session: &wtdb.SessionInfo{
Policy: wtpolicy.Policy{
BlobType: blobType,
SweepFeeRate: sweepFeeRate,
RewardRate: 10000,
},
RewardAddress: rewardScript,
},
bindErr: bindErr,
expSweepScript: makeAddrSlice(22),
signer: signer,
}
}
var (
blobTypeCommitNoReward = blob.FlagCommitOutputs.Type()
blobTypeCommitReward = (blob.FlagCommitOutputs | blob.FlagReward).Type()
addr, _ = btcutil.DecodeAddress(
"mrX9vMRYLfVy1BnZbc5gZjuyaqH3ZW2ZHz", &chaincfg.TestNet3Params,
)
addrScript, _ = txscript.PayToAddrScript(addr)
)
var backupTaskTests = []backupTaskTest{
genTaskTest(
"commit no-reward, both outputs",
100, // stateNum
200000, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate
nil, // rewardScript
299241, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit no-reward, to-local output only",
1000, // stateNum
200000, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate
nil, // rewardScript
199514, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit no-reward, to-remote output only",
1, // stateNum
0, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate
nil, // rewardScript
99561, // expSweepAmt
0, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit no-reward, to-remote output only, creates dust",
1, // stateNum
0, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitNoReward, // blobType
227500, // sweepFeeRate
nil, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
),
genTaskTest(
"commit no-reward, no outputs, fee rate exceeds inputs",
300, // stateNum
0, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate
nil, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr
),
genTaskTest(
"commit no-reward, no outputs, fee rate of 0 creates dust",
300, // stateNum
0, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitNoReward, // blobType
0, // sweepFeeRate
nil, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
),
genTaskTest(
"commit reward, both outputs",
100, // stateNum
200000, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitReward, // blobType
1000, // sweepFeeRate
addrScript, // rewardScript
296241, // expSweepAmt
3000, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit reward, to-local output only",
1000, // stateNum
200000, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitReward, // blobType
1000, // sweepFeeRate
addrScript, // rewardScript
197514, // expSweepAmt
2000, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit reward, to-remote output only",
1, // stateNum
0, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitReward, // blobType
1000, // sweepFeeRate
addrScript, // rewardScript
98561, // expSweepAmt
1000, // expRewardAmt
nil, // bindErr
),
genTaskTest(
"commit reward, to-remote output only, creates dust",
1, // stateNum
0, // toLocalAmt
100000, // toRemoteAmt
blobTypeCommitReward, // blobType
225000, // sweepFeeRate
addrScript, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
),
genTaskTest(
"commit reward, no outputs, fee rate exceeds inputs",
300, // stateNum
0, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitReward, // blobType
1000, // sweepFeeRate
addrScript, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr
),
genTaskTest(
"commit reward, no outputs, fee rate of 0 creates dust",
300, // stateNum
0, // toLocalAmt
0, // toRemoteAmt
blobTypeCommitReward, // blobType
0, // sweepFeeRate
addrScript, // rewardScript
0, // expSweepAmt
0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr
),
}
// TestBackupTaskBind tests the initialization and binding of a backupTask to a
// SessionInfo. After a succesfful bind, all parameters of the justice
// transaction should be solidified, so we assert there correctness. In an
// unsuccessful bind, the session-dependent parameters should be unmodified so
// that the backup task can be rescheduled if necessary. Finally, we assert that
// the backup task is able to encrypt a valid justice kit, and that we can
// decrypt it using the breach txid.
func TestBackupTask(t *testing.T) {
t.Parallel()
for _, test := range backupTaskTests {
t.Run(test.name, func(t *testing.T) {
testBackupTask(t, test)
})
}
}
func testBackupTask(t *testing.T, test backupTaskTest) {
// Create a new backupTask from the channel id and breach info.
task := newBackupTask(&test.chanID, test.breachInfo)
// Assert that all parameters set during initialization are properly
// populated.
if task.chanID != test.chanID {
t.Fatalf("channel id mismatch, want: %s, got: %s",
test.chanID, task.chanID)
}
if task.commitHeight != test.breachInfo.RevokedStateNum {
t.Fatalf("commit height mismatch, want: %d, got: %d",
test.breachInfo.RevokedStateNum, task.commitHeight)
}
if task.totalAmt != test.expTotalAmt {
t.Fatalf("total amount mismatch, want: %d, got: %v",
test.expTotalAmt, task.totalAmt)
}
if !reflect.DeepEqual(task.breachInfo, test.breachInfo) {
t.Fatalf("breach info mismatch, want: %v, got: %v",
test.breachInfo, task.breachInfo)
}
if !reflect.DeepEqual(task.toLocalInput, test.expToLocalInput) {
t.Fatalf("to-local input mismatch, want: %v, got: %v",
test.expToLocalInput, task.toLocalInput)
}
if !reflect.DeepEqual(task.toRemoteInput, test.expToRemoteInput) {
t.Fatalf("to-local input mismatch, want: %v, got: %v",
test.expToRemoteInput, task.toRemoteInput)
}
// Reconstruct the expected input.Inputs that will be returned by the
// task's inputs() method.
expInputs := make(map[wire.OutPoint]input.Input)
if task.toLocalInput != nil {
expInputs[*task.toLocalInput.OutPoint()] = task.toLocalInput
}
if task.toRemoteInput != nil {
expInputs[*task.toRemoteInput.OutPoint()] = task.toRemoteInput
}
// Assert that the inputs method returns the correct slice of
// input.Inputs.
inputs := task.inputs()
if !reflect.DeepEqual(expInputs, inputs) {
t.Fatalf("inputs mismatch, want: %v, got: %v",
expInputs, inputs)
}
// Now, bind the session to the task. If successful, this locks in the
// session's negotiated parameters and allows the backup task to derive
// the final free variables in the justice transaction.
err := task.bindSession(test.session, test.expSweepScript)
if err != test.bindErr {
t.Fatalf("expected: %v when binding session, got: %v",
test.bindErr, err)
}
// Exit early if the bind was supposed to fail. But first, we check that
// all fields set during a bind are still unset. This ensure that a
// failed bind doesn't have side-effects if the task is retried with a
// different session.
if test.bindErr != nil {
if task.blobType != 0 {
t.Fatalf("blob type should not be set on failed bind, "+
"found: %s", task.blobType)
}
if task.outputs != nil {
t.Fatalf("justice outputs should not be set on failed bind, "+
"found: %v", task.outputs)
}
return
}
// Otherwise, the binding succeeded. Assert that all values set during
// the bind are properly populated.
policy := test.session.Policy
if task.blobType != policy.BlobType {
t.Fatalf("blob type mismatch, want: %s, got %s",
policy.BlobType, task.blobType)
}
// Compute the expected outputs on the justice transaction.
var expOutputs = []*wire.TxOut{
{
PkScript: test.expSweepScript,
Value: test.expSweepAmt,
},
}
// If the policy specifies a reward output, add it to the expected list
// of outputs.
if test.session.Policy.BlobType.Has(blob.FlagReward) {
expOutputs = append(expOutputs, &wire.TxOut{
PkScript: test.expRewardScript,
Value: test.expRewardAmt,
})
}
// Assert that the computed outputs match our expected outputs.
if !reflect.DeepEqual(expOutputs, task.outputs) {
t.Fatalf("justice txn output mismatch, want: %v,\ngot: %v",
spew.Sdump(expOutputs), spew.Sdump(task.outputs))
}
// Now, we'll construct, sign, and encrypt the blob containing the parts
// needed to reconstruct the justice transaction.
hint, encBlob, err := task.craftSessionPayload(
test.expSweepScript, test.signer,
)
if err != nil {
t.Fatalf("unable to craft session payload: %v", err)
}
// Verify that the breach hint matches the breach txid's prefix.
breachTxID := test.breachInfo.BreachTransaction.TxHash()
expHint := wtdb.NewBreachHintFromHash(&breachTxID)
if hint != expHint {
t.Fatalf("breach hint mismatch, want: %x, got: %v",
expHint, hint)
}
// Decrypt the return blob to obtain the JusticeKit containing its
// contents.
jKit, err := blob.Decrypt(breachTxID[:], encBlob, policy.BlobType)
if err != nil {
t.Fatalf("unable to decrypt blob: %v", err)
}
keyRing := test.breachInfo.KeyRing
expToLocalPK := keyRing.DelayKey.SerializeCompressed()
expRevPK := keyRing.RevocationKey.SerializeCompressed()
expToRemotePK := keyRing.NoDelayKey.SerializeCompressed()
// Assert that the blob contained the serialized revocation and to-local
// pubkeys.
if !bytes.Equal(jKit.RevocationPubKey[:], expRevPK) {
t.Fatalf("revocation pk mismatch, want: %x, got: %x",
expRevPK, jKit.RevocationPubKey[:])
}
if !bytes.Equal(jKit.LocalDelayPubKey[:], expToLocalPK) {
t.Fatalf("revocation pk mismatch, want: %x, got: %x",
expToLocalPK, jKit.LocalDelayPubKey[:])
}
// Determine if the breach transaction has a to-remote output and/or
// to-local output to spend from. Note the seemingly-reversed
// nomenclature.
hasToRemote := test.breachInfo.LocalOutputSignDesc != nil
hasToLocal := test.breachInfo.RemoteOutputSignDesc != nil
// If the to-remote output is present, assert that the to-remote public
// key was included in the blob.
if hasToRemote &&
!bytes.Equal(jKit.CommitToRemotePubKey[:], expToRemotePK) {
t.Fatalf("mismatch to-remote pubkey, want: %x, got: %x",
expToRemotePK, jKit.CommitToRemotePubKey)
}
// Otherwise if the to-local output is not present, assert that a blank
// public key was inserted.
if !hasToRemote &&
!bytes.Equal(jKit.CommitToRemotePubKey[:], zeroPK[:]) {
t.Fatalf("mismatch to-remote pubkey, want: %x, got: %x",
zeroPK, jKit.CommitToRemotePubKey)
}
// Assert that the CSV is encoded in the blob.
if jKit.CSVDelay != test.breachInfo.RemoteDelay {
t.Fatalf("mismatch remote delay, want: %d, got: %v",
test.breachInfo.RemoteDelay, jKit.CSVDelay)
}
// Assert that the sweep pkscript is included.
if !bytes.Equal(jKit.SweepAddress, test.expSweepScript) {
t.Fatalf("sweep pkscript mismatch, want: %x, got: %x",
test.expSweepScript, jKit.SweepAddress)
}
// Finally, verify that the signatures are encoded in the justice kit.
// We don't validate the actual signatures produced here, since at the
// moment, it is tested indirectly by other packages and integration
// tests.
// TODO(conner): include signature validation checks
emptyToLocalSig := bytes.Equal(jKit.CommitToLocalSig[:], zeroSig[:])
switch {
case hasToLocal && emptyToLocalSig:
t.Fatalf("to-local signature should not be empty")
case !hasToLocal && !emptyToLocalSig:
t.Fatalf("to-local signature should be empty")
}
emptyToRemoteSig := bytes.Equal(jKit.CommitToRemoteSig[:], zeroSig[:])
switch {
case hasToRemote && emptyToRemoteSig:
t.Fatalf("to-remote signature should not be empty")
case !hasToRemote && !emptyToRemoteSig:
t.Fatalf("to-remote signature should be empty")
}
}