Merge pull request #2516 from cfromknecht/wtclient-backup-task
watchtower: client justice transaction creation + blob encryption
This commit is contained in:
commit
9cd88a04b7
@ -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
|
||||
|
296
watchtower/wtclient/backup_task.go
Normal file
296
watchtower/wtclient/backup_task.go
Normal file
@ -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
|
||||
}
|
605
watchtower/wtclient/backup_task_internal_test.go
Normal file
605
watchtower/wtclient/backup_task_internal_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user