Skip to content

Commit 8f98522

Browse files
committed
watchtower/wtclient/backup_task: adds backupTask
1 parent f1f5241 commit 8f98522

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed

watchtower/wtclient/backup_task.go

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

0 commit comments

Comments
 (0)