|
| 1 | +package wtclient |
| 2 | + |
| 3 | +import ( |
| 4 | + "github.com/btcsuite/btcd/blockchain" |
| 5 | + "github.com/btcsuite/btcd/btcec" |
| 6 | + "github.com/btcsuite/btcd/txscript" |
| 7 | + "github.com/btcsuite/btcd/wire" |
| 8 | + "github.com/btcsuite/btcutil" |
| 9 | + "github.com/btcsuite/btcutil/txsort" |
| 10 | + "github.com/lightningnetwork/lnd/input" |
| 11 | + "github.com/lightningnetwork/lnd/lnwallet" |
| 12 | + "github.com/lightningnetwork/lnd/lnwire" |
| 13 | + "github.com/lightningnetwork/lnd/watchtower/blob" |
| 14 | + "github.com/lightningnetwork/lnd/watchtower/wtdb" |
| 15 | +) |
| 16 | + |
| 17 | +// backupTask is an internal struct for computing the justice transaction for a |
| 18 | +// particular revoked state. A backupTask functions as a scratch pad for storing |
| 19 | +// computing values of the transaction itself, such as the final split in |
| 20 | +// balance if the justice transaction will give a reward to the tower. The |
| 21 | +// backup task has three primary phases: |
| 22 | +// 1. Init: Determines which inputs from the breach transaction will be spent, |
| 23 | +// and the total amount contained in the inputs. |
| 24 | +// 2. Bind: Asserts that the revoked state is eligible under a given session's |
| 25 | +// parameters. Certain states may be ineligible due to fee rates, too little |
| 26 | +// input amount, etc. Backup of these states can be deferred to a later time |
| 27 | +// or session with more favorable parameters. If the session is bound |
| 28 | +// successfully, the final session-dependent values to the justice |
| 29 | +// transaction are solidified. |
| 30 | +// 3. Send: Once the task is bound, it will be queued to send to a specific |
| 31 | +// tower corresponding to the session in which it was bound. The justice |
| 32 | +// transaction will be assembled by examining the parameters left as a |
| 33 | +// result of the binding. After the justice transaction is signed, the |
| 34 | +// necessary components are stripped out and encrypted before being sent to |
| 35 | +// the tower in a StateUpdate. |
| 36 | +type backupTask struct { |
| 37 | + chanID lnwire.ChannelID |
| 38 | + commitHeight uint64 |
| 39 | + breachInfo *lnwallet.BreachRetribution |
| 40 | + |
| 41 | + // state-dependent variables |
| 42 | + |
| 43 | + toLocalInput input.Input |
| 44 | + toRemoteInput input.Input |
| 45 | + totalAmt btcutil.Amount |
| 46 | + |
| 47 | + // session-dependent variables |
| 48 | + |
| 49 | + blobType blob.Type |
| 50 | + outputs []*wire.TxOut |
| 51 | +} |
| 52 | + |
| 53 | +// newBackupTask initializes a new backupTask and populates all state-dependent |
| 54 | +// variables. |
| 55 | +func newBackupTask(chanID *lnwire.ChannelID, |
| 56 | + breachInfo *lnwallet.BreachRetribution) *backupTask { |
| 57 | + |
| 58 | + // Parse the non-dust outputs from the breach transaction, |
| 59 | + // simultaneously computing the total amount contained in the inputs |
| 60 | + // present. We can't compute the exact output values at this time |
| 61 | + // since the task has not been assigned to a session, at which point |
| 62 | + // parameters such as fee rate, number of outputs, and reward rate will |
| 63 | + // be finalized. |
| 64 | + var ( |
| 65 | + totalAmt int64 |
| 66 | + toLocalInput input.Input |
| 67 | + toRemoteInput input.Input |
| 68 | + ) |
| 69 | + |
| 70 | + // Add the sign descriptors and outputs corresponding to the to-local |
| 71 | + // and to-remote outputs, respectively, if either input amount is |
| 72 | + // non-dust. Note that the naming here seems reversed, but both are |
| 73 | + // correct. For example, the to-remote output on the remote party's |
| 74 | + // commitment is an output that pays to us. Hence the retribution refers |
| 75 | + // to that output as local, though relative to their commitment, it is |
| 76 | + // paying to-the-remote party (which is us). |
| 77 | + if breachInfo.RemoteOutputSignDesc != nil { |
| 78 | + toLocalInput = input.NewBaseInput( |
| 79 | + &breachInfo.RemoteOutpoint, |
| 80 | + input.CommitmentRevoke, |
| 81 | + breachInfo.RemoteOutputSignDesc, |
| 82 | + 0, |
| 83 | + ) |
| 84 | + totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value |
| 85 | + } |
| 86 | + if breachInfo.LocalOutputSignDesc != nil { |
| 87 | + toRemoteInput = input.NewBaseInput( |
| 88 | + &breachInfo.LocalOutpoint, |
| 89 | + input.CommitmentNoDelay, |
| 90 | + breachInfo.LocalOutputSignDesc, |
| 91 | + 0, |
| 92 | + ) |
| 93 | + totalAmt += breachInfo.LocalOutputSignDesc.Output.Value |
| 94 | + } |
| 95 | + |
| 96 | + return &backupTask{ |
| 97 | + chanID: *chanID, |
| 98 | + commitHeight: breachInfo.RevokedStateNum, |
| 99 | + breachInfo: breachInfo, |
| 100 | + toLocalInput: toLocalInput, |
| 101 | + toRemoteInput: toRemoteInput, |
| 102 | + totalAmt: btcutil.Amount(totalAmt), |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +// inputs returns all non-dust inputs that we will attempt to spend from. |
| 107 | +// |
| 108 | +// NOTE: Ordering of the inputs is not critical as we sort the transaction with |
| 109 | +// BIP69. |
| 110 | +func (t *backupTask) inputs() map[wire.OutPoint]input.Input { |
| 111 | + inputs := make(map[wire.OutPoint]input.Input) |
| 112 | + if t.toLocalInput != nil { |
| 113 | + inputs[*t.toLocalInput.OutPoint()] = t.toLocalInput |
| 114 | + } |
| 115 | + if t.toRemoteInput != nil { |
| 116 | + inputs[*t.toRemoteInput.OutPoint()] = t.toRemoteInput |
| 117 | + } |
| 118 | + return inputs |
| 119 | +} |
| 120 | + |
| 121 | +// bindSession determines if the backupTask is compatible with the passed |
| 122 | +// SessionInfo's policy. If no error is returned, the task has been bound to the |
| 123 | +// session and can be queued to upload to the tower. Otherwise, the bind failed |
| 124 | +// and should be rescheduled with a different session. |
| 125 | +func (t *backupTask) bindSession(session *wtdb.SessionInfo, |
| 126 | + sweepPkScript []byte) error { |
| 127 | + |
| 128 | + // First we'll begin by deriving a weight estimate for the justice |
| 129 | + // transaction. The final weight can be different depending on whether |
| 130 | + // the watchtower is taking a reward. |
| 131 | + var weightEstimate input.TxWeightEstimator |
| 132 | + |
| 133 | + // All justice transactions have a p2wkh output paying to the victim. |
| 134 | + weightEstimate.AddP2WKHOutput() |
| 135 | + |
| 136 | + // Next, add the contribution from the inputs that are present on this |
| 137 | + // breach transaction. |
| 138 | + if t.toLocalInput != nil { |
| 139 | + weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize) |
| 140 | + } |
| 141 | + if t.toRemoteInput != nil { |
| 142 | + weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) |
| 143 | + } |
| 144 | + |
| 145 | + // Now, compute the output values depending on whether FlagReward is set |
| 146 | + // in the current session's policy. |
| 147 | + outputs, err := session.Policy.ComputeJusticeTxOuts( |
| 148 | + t.totalAmt, int64(weightEstimate.Weight()), |
| 149 | + sweepPkScript, session.RewardAddress, |
| 150 | + ) |
| 151 | + if err != nil { |
| 152 | + return err |
| 153 | + } |
| 154 | + |
| 155 | + t.outputs = outputs |
| 156 | + t.blobType = session.Policy.BlobType |
| 157 | + |
| 158 | + return nil |
| 159 | +} |
| 160 | + |
| 161 | +// craftSessionPayload is the final stage for a backupTask, and generates the |
| 162 | +// encrypted payload and breach hint that should be sent to the tower. This |
| 163 | +// method computes the final justice transaction using the bound |
| 164 | +// session-dependent variables, and signs the resulting transaction. The |
| 165 | +// required pieces from signatures, witness scripts, etc are then packaged into |
| 166 | +// a JusticeKit and encrypted using the breach transaction's key. |
| 167 | +func (t *backupTask) craftSessionPayload(sweepPkScript []byte, |
| 168 | + signer input.Signer) (wtdb.BreachHint, []byte, error) { |
| 169 | + |
| 170 | + var hint wtdb.BreachHint |
| 171 | + |
| 172 | + // First, copy over the sweep pkscript, the pubkeys used to derive the |
| 173 | + // to-local script, and the remote CSV delay. |
| 174 | + keyRing := t.breachInfo.KeyRing |
| 175 | + justiceKit := &blob.JusticeKit{ |
| 176 | + SweepAddress: sweepPkScript, |
| 177 | + RevocationPubKey: toBlobPubKey(keyRing.RevocationKey), |
| 178 | + LocalDelayPubKey: toBlobPubKey(keyRing.DelayKey), |
| 179 | + CSVDelay: t.breachInfo.RemoteDelay, |
| 180 | + } |
| 181 | + |
| 182 | + // If this commitment has an output that pays to us, copy the to-remote |
| 183 | + // pubkey into the justice kit. This serves as the indicator to the |
| 184 | + // tower that we expect the breaching transaction to have a non-dust |
| 185 | + // output to spend from. |
| 186 | + if t.toRemoteInput != nil { |
| 187 | + justiceKit.CommitToRemotePubKey = toBlobPubKey( |
| 188 | + keyRing.NoDelayKey, |
| 189 | + ) |
| 190 | + } |
| 191 | + |
| 192 | + // Now, begin construction of the justice transaction. We'll start with |
| 193 | + // a version 2 transaction. |
| 194 | + justiceTxn := wire.NewMsgTx(2) |
| 195 | + |
| 196 | + // Next, add the non-dust inputs that were derived from the breach |
| 197 | + // information. This will either be contain both the to-local and |
| 198 | + // to-remote outputs, or only be the to-local output. |
| 199 | + inputs := t.inputs() |
| 200 | + for prevOutPoint := range inputs { |
| 201 | + justiceTxn.AddTxIn(&wire.TxIn{ |
| 202 | + PreviousOutPoint: prevOutPoint, |
| 203 | + }) |
| 204 | + } |
| 205 | + |
| 206 | + // Add the sweep output paying directly to the user and possibly a |
| 207 | + // reward output, using the outputs computed when the task was bound. |
| 208 | + justiceTxn.TxOut = t.outputs |
| 209 | + |
| 210 | + // Sort the justice transaction according to BIP69. |
| 211 | + txsort.InPlaceSort(justiceTxn) |
| 212 | + |
| 213 | + // Check that the justice transaction meets basic validity requirements |
| 214 | + // before attempting to attach the witnesses. |
| 215 | + btx := btcutil.NewTx(justiceTxn) |
| 216 | + if err := blockchain.CheckTransactionSanity(btx); err != nil { |
| 217 | + return hint, nil, err |
| 218 | + } |
| 219 | + |
| 220 | + // Construct a sighash cache to improve signing performance. |
| 221 | + hashCache := txscript.NewTxSigHashes(justiceTxn) |
| 222 | + |
| 223 | + // Since the transaction inputs could have been reordered as a result of |
| 224 | + // the BIP69 sort, create an index mapping each prevout to it's new |
| 225 | + // index. |
| 226 | + inputIndex := make(map[wire.OutPoint]int) |
| 227 | + for i, txIn := range justiceTxn.TxIn { |
| 228 | + inputIndex[txIn.PreviousOutPoint] = i |
| 229 | + } |
| 230 | + |
| 231 | + // Now, iterate through the list of inputs that were initially added to |
| 232 | + // the transaction and store the computed witness within the justice |
| 233 | + // kit. |
| 234 | + for _, inp := range inputs { |
| 235 | + // Lookup the input's new post-sort position. |
| 236 | + i := inputIndex[*inp.OutPoint()] |
| 237 | + |
| 238 | + // Construct the full witness required to spend this input. |
| 239 | + inputScript, err := inp.CraftInputScript( |
| 240 | + signer, justiceTxn, hashCache, i, |
| 241 | + ) |
| 242 | + if err != nil { |
| 243 | + return hint, nil, err |
| 244 | + } |
| 245 | + |
| 246 | + // Parse the DER-encoded signature from the first position of |
| 247 | + // the resulting witness. We trim an extra byte to remove the |
| 248 | + // sighash flag. |
| 249 | + witness := inputScript.Witness |
| 250 | + rawSignature := witness[0][:len(witness[0])-1] |
| 251 | + |
| 252 | + // Reencode the DER signature into a fixed-size 64 byte |
| 253 | + // signature. |
| 254 | + signature, err := lnwire.NewSigFromRawSignature(rawSignature) |
| 255 | + if err != nil { |
| 256 | + return hint, nil, err |
| 257 | + } |
| 258 | + |
| 259 | + // Finally, copy the serialized signature into the justice kit, |
| 260 | + // using the input's witness type to select the appropriate |
| 261 | + // field. |
| 262 | + switch inp.WitnessType() { |
| 263 | + case input.CommitmentRevoke: |
| 264 | + copy(justiceKit.CommitToLocalSig[:], signature[:]) |
| 265 | + |
| 266 | + case input.CommitmentNoDelay: |
| 267 | + copy(justiceKit.CommitToRemoteSig[:], signature[:]) |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + // Compute the breach hint from the breach transaction id's prefix. |
| 272 | + breachKey := t.breachInfo.BreachTransaction.TxHash() |
| 273 | + |
| 274 | + // Then, we'll encrypt the computed justice kit using the full breach |
| 275 | + // transaction id, which will allow the tower to recover the contents |
| 276 | + // after the transaction is seen in the chain or mempool. |
| 277 | + encBlob, err := justiceKit.Encrypt(breachKey[:], t.blobType) |
| 278 | + if err != nil { |
| 279 | + return hint, nil, err |
| 280 | + } |
| 281 | + |
| 282 | + // Finally, compute the breach hint, taken as the first half of the |
| 283 | + // breach transactions txid. Once the tower sees the breach transaction |
| 284 | + // on the network, it can use the full txid to decyrpt the blob. |
| 285 | + hint = wtdb.NewBreachHintFromHash(&breachKey) |
| 286 | + |
| 287 | + return hint, encBlob, nil |
| 288 | +} |
| 289 | + |
| 290 | +// toBlobPubKey serializes the given pubkey into a blob.PubKey that can be set |
| 291 | +// as a field on a blob.JusticeKit. |
| 292 | +func toBlobPubKey(pubKey *btcec.PublicKey) blob.PubKey { |
| 293 | + var blobPubKey blob.PubKey |
| 294 | + copy(blobPubKey[:], pubKey.SerializeCompressed()) |
| 295 | + return blobPubKey |
| 296 | +} |
0 commit comments