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