diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go new file mode 100644 index 00000000..813cc45c --- /dev/null +++ b/watchtower/wtclient/backup_task.go @@ -0,0 +1,299 @@ +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 + //sweepAmt int64 + //rewardAmt int64 + //rewardAddress []byte +} + +// 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 +}