Skip to content

Commit 9cd88a0

Browse files
authored
Merge pull request #2516 from cfromknecht/wtclient-backup-task
watchtower: client justice transaction creation + blob encryption
2 parents c05a7c5 + e13d88f commit 9cd88a0

File tree

3 files changed

+913
-0
lines changed

3 files changed

+913
-0
lines changed

input/input.go

+12
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ func MakeBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
9595
}
9696
}
9797

98+
// NewBaseInput allocates and assembles a new *BaseInput that can be used to
99+
// construct a sweep transaction.
100+
func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
101+
signDescriptor *SignDescriptor, heightHint uint32) *BaseInput {
102+
103+
input := MakeBaseInput(
104+
outpoint, witnessType, signDescriptor, heightHint,
105+
)
106+
107+
return &input
108+
}
109+
98110
// CraftInputScript returns a valid set of input scripts allowing this output
99111
// to be spent. The returns input scripts should target the input at location
100112
// txIndex within the passed transaction. The input scripts generated by this

watchtower/wtclient/backup_task.go

+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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

Comments
 (0)