github.com/decred/dcrlnd@v0.7.6/watchtower/wtclient/backup_task_internal_test.go (about)

     1  package wtclient
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"io"
     7  	"reflect"
     8  	"testing"
     9  
    10  	"github.com/davecgh/go-spew/spew"
    11  	"github.com/decred/dcrd/chaincfg/v3"
    12  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    13  	"github.com/decred/dcrd/dcrutil/v4"
    14  	"github.com/decred/dcrd/txscript/v4"
    15  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    16  	"github.com/decred/dcrd/wire"
    17  	"github.com/decred/dcrlnd/channeldb"
    18  	"github.com/decred/dcrlnd/input"
    19  	"github.com/decred/dcrlnd/keychain"
    20  	"github.com/decred/dcrlnd/lnwallet"
    21  	"github.com/decred/dcrlnd/lnwallet/chainfee"
    22  	"github.com/decred/dcrlnd/lnwire"
    23  	"github.com/decred/dcrlnd/watchtower/blob"
    24  	"github.com/decred/dcrlnd/watchtower/wtdb"
    25  	"github.com/decred/dcrlnd/watchtower/wtmock"
    26  	"github.com/decred/dcrlnd/watchtower/wtpolicy"
    27  )
    28  
    29  const csvDelay uint32 = 144
    30  
    31  var (
    32  	zeroPK  [33]byte
    33  	zeroSig [64]byte
    34  
    35  	revPrivBytes = []byte{
    36  		0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f,
    37  		0x74, 0x6c, 0x9d, 0x5c, 0xae, 0x88, 0x2d, 0x31,
    38  		0x06, 0x90, 0xdd, 0x8c, 0x9b, 0x31, 0xbc, 0xd1,
    39  		0x78, 0x91, 0x88, 0x2a, 0xf9, 0x74, 0xa0, 0xef,
    40  	}
    41  
    42  	toLocalPrivBytes = []byte{
    43  		0xde, 0x17, 0xc1, 0x2f, 0xdc, 0x1b, 0xc0, 0xc6,
    44  		0x59, 0x5d, 0xf9, 0xc1, 0x3e, 0x89, 0xbc, 0x6f,
    45  		0x01, 0x85, 0x45, 0x76, 0x26, 0xce, 0x9c, 0x55,
    46  		0x3b, 0xc9, 0xec, 0x3d, 0xd8, 0x8b, 0xac, 0xa8,
    47  	}
    48  
    49  	toRemotePrivBytes = []byte{
    50  		0x28, 0x59, 0x6f, 0x36, 0xb8, 0x9f, 0x19, 0x5d,
    51  		0xcb, 0x07, 0x48, 0x8a, 0xe5, 0x89, 0x71, 0x74,
    52  		0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe,
    53  		0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f,
    54  	}
    55  )
    56  
    57  func makeAddrSlice(size int) []byte {
    58  	if size > 75 {
    59  		// This is needed because decred requires a valid script in the
    60  		// pkscript. To simpify tests, we generate a dummy OP_RETURN
    61  		// with a data push so that the total pkscript size is the
    62  		// parametrized size.
    63  		panic("wrong usage")
    64  	}
    65  	addr := make([]byte, size)
    66  	addr[0] = 0x6a // OP_RETURN
    67  	addr[1] = byte(size - 2)
    68  	if _, err := io.ReadFull(rand.Reader, addr[2:]); err != nil {
    69  		panic("cannot make addr")
    70  	}
    71  	return addr
    72  }
    73  
    74  type backupTaskTest struct {
    75  	name             string
    76  	chanID           lnwire.ChannelID
    77  	breachInfo       *lnwallet.BreachRetribution
    78  	expToLocalInput  input.Input
    79  	expToRemoteInput input.Input
    80  	expTotalAmt      dcrutil.Amount
    81  	expSweepAmt      int64
    82  	expRewardAmt     int64
    83  	expRewardScript  []byte
    84  	session          *wtdb.ClientSessionBody
    85  	bindErr          error
    86  	expSweepScript   []byte
    87  	signer           input.Signer
    88  	chanType         channeldb.ChannelType
    89  }
    90  
    91  func privKeyFromBytes(b []byte) (*secp256k1.PrivateKey, *secp256k1.PublicKey) {
    92  	p := secp256k1.PrivKeyFromBytes(b)
    93  	return p, p.PubKey()
    94  }
    95  
    96  // genTaskTest creates a instance of a backupTaskTest using the passed
    97  // parameters. This method handles generating a breach transaction and its
    98  // corresponding BreachInfo, as well as setting the wtpolicy.Policy of the given
    99  // session.
   100  func genTaskTest(
   101  	name string,
   102  	stateNum uint64,
   103  	toLocalAmt int64,
   104  	toRemoteAmt int64,
   105  	blobType blob.Type,
   106  	sweepFeeRate chainfee.AtomPerKByte,
   107  	rewardScript []byte,
   108  	expSweepAmt int64,
   109  	expRewardAmt int64,
   110  	bindErr error,
   111  	chanType channeldb.ChannelType) backupTaskTest {
   112  
   113  	// Set the anchor flag in the blob type if the session needs to support
   114  	// anchor channels.
   115  	if chanType.HasAnchors() {
   116  		blobType |= blob.Type(blob.FlagAnchorChannel)
   117  	}
   118  
   119  	// Parse the key pairs for all keys used in the test.
   120  	revSK, revPK := privKeyFromBytes(
   121  		revPrivBytes,
   122  	)
   123  	_, toLocalPK := privKeyFromBytes(
   124  		toLocalPrivBytes,
   125  	)
   126  	toRemoteSK, toRemotePK := privKeyFromBytes(
   127  		toRemotePrivBytes,
   128  	)
   129  
   130  	// Create the signer, and add the revocation and to-remote privkeys.
   131  	signer := wtmock.NewMockSigner()
   132  	var (
   133  		revKeyLoc      = signer.AddPrivKey(revSK)
   134  		toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK)
   135  	)
   136  
   137  	// First, we'll initialize a new breach transaction and the
   138  	// corresponding breach retribution. The retribution stores a pointer to
   139  	// the breach transaction, which we will continue to modify.
   140  	breachTxn := wire.NewMsgTx()
   141  	breachTxn.Version = 2
   142  	breachInfo := &lnwallet.BreachRetribution{
   143  		RevokedStateNum:   stateNum,
   144  		BreachTransaction: breachTxn,
   145  		KeyRing: &lnwallet.CommitmentKeyRing{
   146  			RevocationKey: revPK,
   147  			ToLocalKey:    toLocalPK,
   148  			ToRemoteKey:   toRemotePK,
   149  		},
   150  		RemoteDelay: csvDelay,
   151  	}
   152  
   153  	// Add the sign descriptors and outputs corresponding to the to-local
   154  	// and to-remote outputs, respectively, if either input amount is
   155  	// non-zero. Note that the naming here seems reversed, but both are
   156  	// correct. For example, the to-remote output on the remote party's
   157  	// commitment is an output that pays to us. Hence the retribution refers
   158  	// to that output as local, though relative to their commitment, it is
   159  	// paying to-the-remote party (which is us).
   160  	if toLocalAmt > 0 {
   161  		toLocalSignDesc := &input.SignDescriptor{
   162  			KeyDesc: keychain.KeyDescriptor{
   163  				KeyLocator: revKeyLoc,
   164  				PubKey:     revPK,
   165  			},
   166  			Output: &wire.TxOut{
   167  				Value: toLocalAmt,
   168  			},
   169  			HashType: txscript.SigHashAll,
   170  		}
   171  		breachInfo.RemoteOutputSignDesc = toLocalSignDesc
   172  		breachTxn.AddTxOut(toLocalSignDesc.Output)
   173  	}
   174  	if toRemoteAmt > 0 {
   175  		toRemoteSignDesc := &input.SignDescriptor{
   176  			KeyDesc: keychain.KeyDescriptor{
   177  				KeyLocator: toRemoteKeyLoc,
   178  				PubKey:     toRemotePK,
   179  			},
   180  			Output: &wire.TxOut{
   181  				Value: toRemoteAmt,
   182  			},
   183  			HashType: txscript.SigHashAll,
   184  		}
   185  		breachInfo.LocalOutputSignDesc = toRemoteSignDesc
   186  		breachTxn.AddTxOut(toRemoteSignDesc.Output)
   187  	}
   188  
   189  	var (
   190  		toLocalInput  input.Input
   191  		toRemoteInput input.Input
   192  	)
   193  
   194  	// Now that the breach transaction has all its outputs, we can compute
   195  	// its txid and inputs spending from it. We also generate the
   196  	// input.Inputs that should be derived by the backup task.
   197  	txid := breachTxn.TxHash()
   198  	var index uint32
   199  	if toLocalAmt > 0 {
   200  		breachInfo.RemoteOutpoint = wire.OutPoint{
   201  			Hash:  txid,
   202  			Index: index,
   203  		}
   204  		toLocalInput = input.NewBaseInput(
   205  			&breachInfo.RemoteOutpoint,
   206  			input.CommitmentRevoke,
   207  			breachInfo.RemoteOutputSignDesc,
   208  			0,
   209  		)
   210  		index++
   211  	}
   212  	if toRemoteAmt > 0 {
   213  		breachInfo.LocalOutpoint = wire.OutPoint{
   214  			Hash:  txid,
   215  			Index: index,
   216  		}
   217  
   218  		var witnessType input.WitnessType
   219  		switch {
   220  		case chanType.HasAnchors():
   221  			witnessType = input.CommitmentToRemoteConfirmed
   222  		case chanType.IsTweakless():
   223  			witnessType = input.CommitSpendNoDelayTweakless
   224  		default:
   225  			witnessType = input.CommitmentNoDelay
   226  		}
   227  
   228  		if chanType.HasAnchors() {
   229  			toRemoteInput = input.NewCsvInput(
   230  				&breachInfo.LocalOutpoint,
   231  				witnessType,
   232  				breachInfo.LocalOutputSignDesc,
   233  				0, 1,
   234  			)
   235  		} else {
   236  			toRemoteInput = input.NewBaseInput(
   237  				&breachInfo.LocalOutpoint,
   238  				witnessType,
   239  				breachInfo.LocalOutputSignDesc,
   240  				0,
   241  			)
   242  		}
   243  	}
   244  
   245  	return backupTaskTest{
   246  		name:             name,
   247  		breachInfo:       breachInfo,
   248  		expToLocalInput:  toLocalInput,
   249  		expToRemoteInput: toRemoteInput,
   250  		expTotalAmt:      dcrutil.Amount(toLocalAmt + toRemoteAmt),
   251  		expSweepAmt:      expSweepAmt,
   252  		expRewardAmt:     expRewardAmt,
   253  		expRewardScript:  rewardScript,
   254  		session: &wtdb.ClientSessionBody{
   255  			Policy: wtpolicy.Policy{
   256  				TxPolicy: wtpolicy.TxPolicy{
   257  					BlobType:     blobType,
   258  					SweepFeeRate: sweepFeeRate,
   259  					RewardRate:   10000,
   260  				},
   261  			},
   262  			RewardPkScript: rewardScript,
   263  		},
   264  		bindErr:        bindErr,
   265  		expSweepScript: makeAddrSlice(22),
   266  		signer:         signer,
   267  		chanType:       chanType,
   268  	}
   269  }
   270  
   271  var (
   272  	blobTypeCommitNoReward = blob.FlagCommitOutputs.Type()
   273  
   274  	blobTypeCommitReward = (blob.FlagCommitOutputs | blob.FlagReward).Type()
   275  
   276  	addr, _ = stdaddr.DecodeAddress(
   277  		"Tsi6gGYNSMmFwi7JoL5Li39SrERZTTMu6vY",
   278  		chaincfg.TestNet3Params(),
   279  	)
   280  
   281  	addrScript, _ = input.PayToAddrScript(addr)
   282  )
   283  
   284  // TestBackupTaskBind tests the initialization and binding of a backupTask to a
   285  // ClientSession. After a successful bind, all parameters of the justice
   286  // transaction should be solidified, so we assert there correctness. In an
   287  // unsuccessful bind, the session-dependent parameters should be unmodified so
   288  // that the backup task can be rescheduled if necessary. Finally, we assert
   289  // that the backup task is able to encrypt a valid justice kit, and that we can
   290  // decrypt it using the breach txid.
   291  func TestBackupTask(t *testing.T) {
   292  	t.Parallel()
   293  
   294  	chanTypes := []channeldb.ChannelType{
   295  		channeldb.SingleFunderBit,
   296  		channeldb.SingleFunderTweaklessBit,
   297  		channeldb.AnchorOutputsBit,
   298  	}
   299  
   300  	var backupTaskTests []backupTaskTest
   301  	for _, chanType := range chanTypes {
   302  		// Depending on whether the test is for anchor channels or
   303  		// legacy (tweaked and non-tweaked) channels, adjust the
   304  		// expected sweep amount to accommodate. These are different for
   305  		// several reasons:
   306  		//   - anchor to-remote outputs require a P2WSH sweep rather
   307  		//     than a P2WKH sweep.
   308  		//   - the to-local weight estimate fixes an off-by-one.
   309  		// In tests related to the dust threshold, the size difference
   310  		// between the channel types makes it so that the threshold fee
   311  		// rate is slightly lower (since the transactions are heavier).
   312  		var (
   313  			expSweepCommitNoRewardBoth     int64                 = 299568
   314  			expSweepCommitNoRewardLocal    int64                 = 199734
   315  			expSweepCommitNoRewardRemote   int64                 = 99783
   316  			expSweepCommitRewardBoth       int64                 = 296532
   317  			expSweepCommitRewardLocal      int64                 = 197698
   318  			expSweepCommitRewardRemote     int64                 = 98747
   319  			sweepFeeRateNoRewardRemoteDust chainfee.AtomPerKByte = 455000
   320  			sweepFeeRateRewardRemoteDust   chainfee.AtomPerKByte = 385000
   321  		)
   322  		if chanType.HasAnchors() {
   323  			expSweepCommitNoRewardBoth = 299564
   324  			expSweepCommitNoRewardLocal = 199734
   325  			expSweepCommitNoRewardRemote = 99779
   326  			expSweepCommitRewardBoth = 296528
   327  			expSweepCommitRewardLocal = 197698
   328  			expSweepCommitRewardRemote = 98743
   329  			sweepFeeRateNoRewardRemoteDust = 450000
   330  			sweepFeeRateRewardRemoteDust = 385000
   331  		}
   332  
   333  		backupTaskTests = append(backupTaskTests, []backupTaskTest{
   334  			genTaskTest(
   335  				"commit no-reward, both outputs",
   336  				100,                        // stateNum
   337  				200000,                     // toLocalAmt
   338  				100000,                     // toRemoteAmt
   339  				blobTypeCommitNoReward,     // blobType
   340  				1000,                       // sweepFeeRate
   341  				nil,                        // rewardScript
   342  				expSweepCommitNoRewardBoth, // expSweepAmt
   343  				0,                          // expRewardAmt
   344  				nil,                        // bindErr
   345  				chanType,
   346  			),
   347  			genTaskTest(
   348  				"commit no-reward, to-local output only",
   349  				1000,                        // stateNum
   350  				200000,                      // toLocalAmt
   351  				0,                           // toRemoteAmt
   352  				blobTypeCommitNoReward,      // blobType
   353  				1000,                        // sweepFeeRate
   354  				nil,                         // rewardScript
   355  				expSweepCommitNoRewardLocal, // expSweepAmt
   356  				0,                           // expRewardAmt
   357  				nil,                         // bindErr
   358  				chanType,
   359  			),
   360  			genTaskTest(
   361  				"commit no-reward, to-remote output only",
   362  				1,                            // stateNum
   363  				0,                            // toLocalAmt
   364  				100000,                       // toRemoteAmt
   365  				blobTypeCommitNoReward,       // blobType
   366  				1000,                         // sweepFeeRate
   367  				nil,                          // rewardScript
   368  				expSweepCommitNoRewardRemote, // expSweepAmt
   369  				0,                            // expRewardAmt
   370  				nil,                          // bindErr
   371  				chanType,
   372  			),
   373  			genTaskTest(
   374  				"commit no-reward, to-remote output only, creates dust",
   375  				1,                              // stateNum
   376  				0,                              // toLocalAmt
   377  				100000,                         // toRemoteAmt
   378  				blobTypeCommitNoReward,         // blobType
   379  				sweepFeeRateNoRewardRemoteDust, // sweepFeeRate
   380  				nil,                            // rewardScript
   381  				0,                              // expSweepAmt
   382  				0,                              // expRewardAmt
   383  				wtpolicy.ErrCreatesDust,        // bindErr
   384  				chanType,
   385  			),
   386  			genTaskTest(
   387  				"commit no-reward, no outputs, fee rate exceeds inputs",
   388  				300,                          // stateNum
   389  				0,                            // toLocalAmt
   390  				0,                            // toRemoteAmt
   391  				blobTypeCommitNoReward,       // blobType
   392  				1000,                         // sweepFeeRate
   393  				nil,                          // rewardScript
   394  				0,                            // expSweepAmt
   395  				0,                            // expRewardAmt
   396  				wtpolicy.ErrFeeExceedsInputs, // bindErr
   397  				chanType,
   398  			),
   399  			genTaskTest(
   400  				"commit no-reward, no outputs, fee rate of 0 creates dust",
   401  				300,                     // stateNum
   402  				0,                       // toLocalAmt
   403  				0,                       // toRemoteAmt
   404  				blobTypeCommitNoReward,  // blobType
   405  				0,                       // sweepFeeRate
   406  				nil,                     // rewardScript
   407  				0,                       // expSweepAmt
   408  				0,                       // expRewardAmt
   409  				wtpolicy.ErrCreatesDust, // bindErr
   410  				chanType,
   411  			),
   412  			genTaskTest(
   413  				"commit reward, both outputs",
   414  				100,                      // stateNum
   415  				200000,                   // toLocalAmt
   416  				100000,                   // toRemoteAmt
   417  				blobTypeCommitReward,     // blobType
   418  				1000,                     // sweepFeeRate
   419  				addrScript,               // rewardScript
   420  				expSweepCommitRewardBoth, // expSweepAmt
   421  				3000,                     // expRewardAmt
   422  				nil,                      // bindErr
   423  				chanType,
   424  			),
   425  			genTaskTest(
   426  				"commit reward, to-local output only",
   427  				1000,                      // stateNum
   428  				200000,                    // toLocalAmt
   429  				0,                         // toRemoteAmt
   430  				blobTypeCommitReward,      // blobType
   431  				1000,                      // sweepFeeRate
   432  				addrScript,                // rewardScript
   433  				expSweepCommitRewardLocal, // expSweepAmt
   434  				2000,                      // expRewardAmt
   435  				nil,                       // bindErr
   436  				chanType,
   437  			),
   438  			genTaskTest(
   439  				"commit reward, to-remote output only",
   440  				1,                          // stateNum
   441  				0,                          // toLocalAmt
   442  				100000,                     // toRemoteAmt
   443  				blobTypeCommitReward,       // blobType
   444  				1000,                       // sweepFeeRate
   445  				addrScript,                 // rewardScript
   446  				expSweepCommitRewardRemote, // expSweepAmt
   447  				1000,                       // expRewardAmt
   448  				nil,                        // bindErr
   449  				chanType,
   450  			),
   451  			genTaskTest(
   452  				"commit reward, to-remote output only, creates dust",
   453  				1,                            // stateNum
   454  				0,                            // toLocalAmt
   455  				100000,                       // toRemoteAmt
   456  				blobTypeCommitReward,         // blobType
   457  				sweepFeeRateRewardRemoteDust, // sweepFeeRate
   458  				addrScript,                   // rewardScript
   459  				0,                            // expSweepAmt
   460  				0,                            // expRewardAmt
   461  				wtpolicy.ErrCreatesDust,      // bindErr
   462  				chanType,
   463  			),
   464  			genTaskTest(
   465  				"commit reward, no outputs, fee rate exceeds inputs",
   466  				300,                          // stateNum
   467  				0,                            // toLocalAmt
   468  				0,                            // toRemoteAmt
   469  				blobTypeCommitReward,         // blobType
   470  				1000,                         // sweepFeeRate
   471  				addrScript,                   // rewardScript
   472  				0,                            // expSweepAmt
   473  				0,                            // expRewardAmt
   474  				wtpolicy.ErrFeeExceedsInputs, // bindErr
   475  				chanType,
   476  			),
   477  			genTaskTest(
   478  				"commit reward, no outputs, fee rate of 0 creates dust",
   479  				300,                     // stateNum
   480  				0,                       // toLocalAmt
   481  				0,                       // toRemoteAmt
   482  				blobTypeCommitReward,    // blobType
   483  				0,                       // sweepFeeRate
   484  				addrScript,              // rewardScript
   485  				0,                       // expSweepAmt
   486  				0,                       // expRewardAmt
   487  				wtpolicy.ErrCreatesDust, // bindErr
   488  				chanType,
   489  			),
   490  		}...)
   491  	}
   492  
   493  	for _, test := range backupTaskTests {
   494  		test := test
   495  
   496  		t.Run(test.name, func(t *testing.T) {
   497  			t.Parallel()
   498  
   499  			testBackupTask(t, test)
   500  		})
   501  	}
   502  }
   503  
   504  func testBackupTask(t *testing.T, test backupTaskTest) {
   505  	// Create a new backupTask from the channel id and breach info.
   506  	task := newBackupTask(
   507  		&test.chanID, test.breachInfo, test.expSweepScript,
   508  		test.chanType, chaincfg.TestNet3Params(),
   509  	)
   510  
   511  	// Assert that all parameters set during initialization are properly
   512  	// populated.
   513  	if task.id.ChanID != test.chanID {
   514  		t.Fatalf("channel id mismatch, want: %s, got: %s",
   515  			test.chanID, task.id.ChanID)
   516  	}
   517  
   518  	if task.id.CommitHeight != test.breachInfo.RevokedStateNum {
   519  		t.Fatalf("commit height mismatch, want: %d, got: %d",
   520  			test.breachInfo.RevokedStateNum, task.id.CommitHeight)
   521  	}
   522  
   523  	if task.totalAmt != test.expTotalAmt {
   524  		t.Fatalf("total amount mismatch, want: %d, got: %v",
   525  			test.expTotalAmt, task.totalAmt)
   526  	}
   527  
   528  	if !reflect.DeepEqual(task.breachInfo, test.breachInfo) {
   529  		t.Fatalf("breach info mismatch, want: %v, got: %v",
   530  			test.breachInfo, task.breachInfo)
   531  	}
   532  
   533  	if !reflect.DeepEqual(task.toLocalInput, test.expToLocalInput) {
   534  		t.Fatalf("to-local input mismatch, want: %v, got: %v",
   535  			test.expToLocalInput, task.toLocalInput)
   536  	}
   537  
   538  	if !reflect.DeepEqual(task.toRemoteInput, test.expToRemoteInput) {
   539  		t.Fatalf("to-local input mismatch, want: %v, got: %v",
   540  			test.expToRemoteInput, task.toRemoteInput)
   541  	}
   542  
   543  	// Reconstruct the expected input.Inputs that will be returned by the
   544  	// task's inputs() method.
   545  	expInputs := make(map[wire.OutPoint]input.Input)
   546  	if task.toLocalInput != nil {
   547  		expInputs[*task.toLocalInput.OutPoint()] = task.toLocalInput
   548  	}
   549  	if task.toRemoteInput != nil {
   550  		expInputs[*task.toRemoteInput.OutPoint()] = task.toRemoteInput
   551  	}
   552  
   553  	// Assert that the inputs method returns the correct slice of
   554  	// input.Inputs.
   555  	inputs := task.inputs()
   556  	if !reflect.DeepEqual(expInputs, inputs) {
   557  		t.Fatalf("inputs mismatch, want: %v, got: %v",
   558  			expInputs, inputs)
   559  	}
   560  
   561  	// Now, bind the session to the task. If successful, this locks in the
   562  	// session's negotiated parameters and allows the backup task to derive
   563  	// the final free variables in the justice transaction.
   564  	err := task.bindSession(test.session)
   565  	if err != test.bindErr {
   566  		t.Fatalf("expected: %v when binding session, got: %v",
   567  			test.bindErr, err)
   568  	}
   569  
   570  	// Exit early if the bind was supposed to fail. But first, we check that
   571  	// all fields set during a bind are still unset. This ensure that a
   572  	// failed bind doesn't have side-effects if the task is retried with a
   573  	// different session.
   574  	if test.bindErr != nil {
   575  		if task.blobType != 0 {
   576  			t.Fatalf("blob type should not be set on failed bind, "+
   577  				"found: %s", task.blobType)
   578  		}
   579  
   580  		if task.outputs != nil {
   581  			t.Fatalf("justice outputs should not be set on failed bind, "+
   582  				"found: %v", task.outputs)
   583  		}
   584  
   585  		return
   586  	}
   587  
   588  	// Otherwise, the binding succeeded. Assert that all values set during
   589  	// the bind are properly populated.
   590  	policy := test.session.Policy
   591  	if task.blobType != policy.BlobType {
   592  		t.Fatalf("blob type mismatch, want: %s, got %s",
   593  			policy.BlobType, task.blobType)
   594  	}
   595  
   596  	// Compute the expected outputs on the justice transaction.
   597  	var expOutputs = []*wire.TxOut{
   598  		{
   599  			PkScript: test.expSweepScript,
   600  			Value:    test.expSweepAmt,
   601  		},
   602  	}
   603  
   604  	// If the policy specifies a reward output, add it to the expected list
   605  	// of outputs.
   606  	if test.session.Policy.BlobType.Has(blob.FlagReward) {
   607  		expOutputs = append(expOutputs, &wire.TxOut{
   608  			PkScript: test.expRewardScript,
   609  			Value:    test.expRewardAmt,
   610  		})
   611  	}
   612  
   613  	// Assert that the computed outputs match our expected outputs.
   614  	if !reflect.DeepEqual(expOutputs, task.outputs) {
   615  		t.Fatalf("justice txn output mismatch, want: %v,\ngot: %v",
   616  			spew.Sdump(expOutputs), spew.Sdump(task.outputs))
   617  	}
   618  
   619  	// Now, we'll construct, sign, and encrypt the blob containing the parts
   620  	// needed to reconstruct the justice transaction.
   621  	hint, encBlob, err := task.craftSessionPayload(test.signer)
   622  	if err != nil {
   623  		t.Fatalf("unable to craft session payload: %v", err)
   624  	}
   625  
   626  	// Verify that the breach hint matches the breach txid's prefix.
   627  	breachTxID := test.breachInfo.BreachTransaction.TxHash()
   628  	expHint := blob.NewBreachHintFromHash(&breachTxID)
   629  	if hint != expHint {
   630  		t.Fatalf("breach hint mismatch, want: %x, got: %v",
   631  			expHint, hint)
   632  	}
   633  
   634  	// Decrypt the return blob to obtain the JusticeKit containing its
   635  	// contents.
   636  	key := blob.NewBreachKeyFromHash(&breachTxID)
   637  	jKit, err := blob.Decrypt(key, encBlob, policy.BlobType)
   638  	if err != nil {
   639  		t.Fatalf("unable to decrypt blob: %v", err)
   640  	}
   641  
   642  	keyRing := test.breachInfo.KeyRing
   643  	expToLocalPK := keyRing.ToLocalKey.SerializeCompressed()
   644  	expRevPK := keyRing.RevocationKey.SerializeCompressed()
   645  	expToRemotePK := keyRing.ToRemoteKey.SerializeCompressed()
   646  
   647  	// Assert that the blob contained the serialized revocation and to-local
   648  	// pubkeys.
   649  	if !bytes.Equal(jKit.RevocationPubKey[:], expRevPK) {
   650  		t.Fatalf("revocation pk mismatch, want: %x, got: %x",
   651  			expRevPK, jKit.RevocationPubKey[:])
   652  	}
   653  	if !bytes.Equal(jKit.LocalDelayPubKey[:], expToLocalPK) {
   654  		t.Fatalf("revocation pk mismatch, want: %x, got: %x",
   655  			expToLocalPK, jKit.LocalDelayPubKey[:])
   656  	}
   657  
   658  	// Determine if the breach transaction has a to-remote output and/or
   659  	// to-local output to spend from. Note the seemingly-reversed
   660  	// nomenclature.
   661  	hasToRemote := test.breachInfo.LocalOutputSignDesc != nil
   662  	hasToLocal := test.breachInfo.RemoteOutputSignDesc != nil
   663  
   664  	// If the to-remote output is present, assert that the to-remote public
   665  	// key was included in the blob.
   666  	if hasToRemote &&
   667  		!bytes.Equal(jKit.CommitToRemotePubKey[:], expToRemotePK) {
   668  		t.Fatalf("mismatch to-remote pubkey, want: %x, got: %x",
   669  			expToRemotePK, jKit.CommitToRemotePubKey)
   670  	}
   671  
   672  	// Otherwise if the to-local output is not present, assert that a blank
   673  	// public key was inserted.
   674  	if !hasToRemote &&
   675  		!bytes.Equal(jKit.CommitToRemotePubKey[:], zeroPK[:]) {
   676  		t.Fatalf("mismatch to-remote pubkey, want: %x, got: %x",
   677  			zeroPK, jKit.CommitToRemotePubKey)
   678  	}
   679  
   680  	// Assert that the CSV is encoded in the blob.
   681  	if jKit.CSVDelay != test.breachInfo.RemoteDelay {
   682  		t.Fatalf("mismatch remote delay, want: %d, got: %v",
   683  			test.breachInfo.RemoteDelay, jKit.CSVDelay)
   684  	}
   685  
   686  	// Assert that the sweep pkscript is included.
   687  	if !bytes.Equal(jKit.SweepAddress, test.expSweepScript) {
   688  		t.Fatalf("sweep pkscript mismatch, want: %x, got: %x",
   689  			test.expSweepScript, jKit.SweepAddress)
   690  	}
   691  
   692  	// Finally, verify that the signatures are encoded in the justice kit.
   693  	// We don't validate the actual signatures produced here, since at the
   694  	// moment, it is tested indirectly by other packages and integration
   695  	// tests.
   696  	// TODO(conner): include signature validation checks
   697  
   698  	emptyToLocalSig := bytes.Equal(jKit.CommitToLocalSig[:], zeroSig[:])
   699  	switch {
   700  	case hasToLocal && emptyToLocalSig:
   701  		t.Fatalf("to-local signature should not be empty")
   702  	case !hasToLocal && !emptyToLocalSig:
   703  		t.Fatalf("to-local signature should be empty")
   704  	}
   705  
   706  	emptyToRemoteSig := bytes.Equal(jKit.CommitToRemoteSig[:], zeroSig[:])
   707  	switch {
   708  	case hasToRemote && emptyToRemoteSig:
   709  		t.Fatalf("to-remote signature should not be empty")
   710  	case !hasToRemote && !emptyToRemoteSig:
   711  		t.Fatalf("to-remote signature should be empty")
   712  	}
   713  }