/
backup_task.go
354 lines (298 loc) · 11.3 KB
/
backup_task.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package wtclient
import (
"fmt"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"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 {
id wtdb.BackupID
breachInfo *lnwallet.BreachRetribution
commitmentType blob.CommitmentType
// state-dependent variables
toLocalInput input.Input
toRemoteInput input.Input
totalAmt btcutil.Amount
sweepPkScript []byte
// session-dependent variables
blobType blob.Type
outputs []*wire.TxOut
}
// newBackupTask initializes a new backupTask.
func newBackupTask(id wtdb.BackupID, sweepPkScript []byte) *backupTask {
return &backupTask{
id: id,
sweepPkScript: sweepPkScript,
}
}
// 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 in a later stage.
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
}
// addrType returns the type of an address after parsing it and matching it to
// the set of known script templates.
func addrType(pkScript []byte) txscript.ScriptClass {
// We pass in a set of dummy chain params here as they're only needed
// to make the address struct, which we're ignoring anyway (scripts are
// always the same, it's addresses that change across chains).
scriptClass, _, _, _ := txscript.ExtractPkScriptAddrs(
pkScript, &chaincfg.MainNetParams,
)
return scriptClass
}
// addScriptWeight parses the passed pkScript and adds the computed weight cost
// were the script to be added to the justice transaction.
func addScriptWeight(weightEstimate *input.TxWeightEstimator,
pkScript []byte) error {
switch addrType(pkScript) {
case txscript.WitnessV0PubKeyHashTy:
weightEstimate.AddP2WKHOutput()
case txscript.WitnessV0ScriptHashTy:
weightEstimate.AddP2WSHOutput()
case txscript.WitnessV1TaprootTy:
weightEstimate.AddP2TROutput()
default:
return fmt.Errorf("invalid addr type: %v", addrType(pkScript))
}
return nil
}
// bindSession first populates all state-dependent variables of the task. Then
// it 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.ClientSessionBody,
newBreachRetribution BreachRetributionBuilder) error {
breachInfo, chanType, err := newBreachRetribution(
t.id.ChanID, t.id.CommitHeight,
)
if err != nil {
return err
}
commitType, err := session.Policy.BlobType.CommitmentType(&chanType)
if err != nil {
return err
}
// 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, err = commitType.ToLocalInput(breachInfo)
if err != nil {
return err
}
totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
}
if breachInfo.LocalOutputSignDesc != nil {
toRemoteInput, err = commitType.ToRemoteInput(breachInfo)
if err != nil {
return err
}
totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
}
t.commitmentType = commitType
t.breachInfo = breachInfo
t.toLocalInput = toLocalInput
t.toRemoteInput = toRemoteInput
t.totalAmt = btcutil.Amount(totalAmt)
// 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
// Next, add the contribution from the inputs that are present on this
// breach transaction.
if t.toLocalInput != nil {
toLocalWitnessSize, err := commitType.ToLocalWitnessSize()
if err != nil {
return err
}
weightEstimate.AddWitnessInput(toLocalWitnessSize)
}
if t.toRemoteInput != nil {
toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize()
if err != nil {
return err
}
weightEstimate.AddWitnessInput(toRemoteWitnessSize)
}
// All justice transactions will either use segwit v0 (p2wkh + p2wsh)
// or segwit v1 (p2tr).
err = addScriptWeight(&weightEstimate, t.sweepPkScript)
if err != nil {
return err
}
// If the justice transaction has a reward output, add the output's
// contribution to the weight estimate.
if session.Policy.BlobType.Has(blob.FlagReward) {
err := addScriptWeight(&weightEstimate, session.RewardPkScript)
if err != nil {
return err
}
}
// 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()),
t.sweepPkScript, session.RewardPkScript,
)
if err != nil {
return err
}
t.blobType = session.Policy.BlobType
t.outputs = outputs
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(
signer input.Signer) (blob.BreachHint, []byte, error) {
var hint blob.BreachHint
justiceKit, err := t.commitmentType.NewJusticeKit(
t.sweepPkScript, t.breachInfo, t.toRemoteInput != nil,
)
if err != nil {
return hint, nil, err
}
// 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()
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(nil)
for prevOutPoint, inp := range inputs {
prevOutputFetcher.AddPrevOut(
prevOutPoint, inp.SignDesc().Output,
)
justiceTxn.AddTxIn(&wire.TxIn{
PreviousOutPoint: prevOutPoint,
Sequence: inp.BlocksToMaturity(),
})
}
// 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, prevOutputFetcher)
// 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.
commitType := t.commitmentType
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, prevOutputFetcher, i,
)
if err != nil {
return hint, nil, err
}
signature, err := commitType.ParseRawSig(inputScript.Witness)
if err != nil {
return hint, nil, err
}
toLocalWitnessType, err := commitType.ToLocalWitnessType()
if err != nil {
return hint, nil, err
}
toRemoteWitnessType, err := commitType.ToRemoteWitnessType()
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 toLocalWitnessType:
justiceKit.AddToLocalSig(signature)
case toRemoteWitnessType:
justiceKit.AddToRemoteSig(signature)
default:
return hint, nil, fmt.Errorf("invalid witness type: %v",
inp.WitnessType())
}
}
breachTxID := t.breachInfo.BreachTxHash
// Compute the breach key as SHA256(txid).
hint, key := blob.NewBreachHintAndKeyFromHash(&breachTxID)
// 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 := blob.Encrypt(justiceKit, key)
if err != nil {
return hint, nil, err
}
return hint, encBlob, nil
}