-
Notifications
You must be signed in to change notification settings - Fork 46
/
batch_verifier.go
402 lines (346 loc) · 12.4 KB
/
batch_verifier.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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package order
import (
"errors"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/terms"
"github.com/lightningnetwork/lnd/lnrpc"
)
const (
// deriveKeyTimeout is the number of seconds we allow the wallet to take
// to derive a key.
deriveKeyTimeout = 10 * time.Second
// heightHintPadding is the padding we subtract/add to our best known
// height to avoid any discrepancies in block propagation between us and
// the auctioneer.
heightHintPadding = 3
)
var (
// ErrMismatchErr is the wrapped error that is returned if the batch
// verification fails.
ErrMismatchErr = errors.New("batch verification result mismatch")
)
// MismatchErr is an error type that is returned if the batch verification on
// the client does not come up with the same result as the server.
type MismatchErr struct {
msg string
cause error
}
// Unwrap returns the underlying error cause. This is always ErrMismatchErr so
// we can compare any error returned by the batch verifier with errors.Is() but
// still retain the context what exactly went wrong.
func (m *MismatchErr) Unwrap() error {
return ErrMismatchErr
}
// Error returns the underlying error message.
//
// NOTE: This method is part of the error interface.
func (m *MismatchErr) Error() string {
if m.cause == nil {
return m.msg
}
return fmt.Sprintf("%s: %v", m.msg, m.cause)
}
// newMismatchErr return a new MismatchErr from the cause and the error message.
func newMismatchErr(cause error, msg string, args ...interface{}) error {
return &MismatchErr{
msg: fmt.Sprintf(msg, args...),
cause: cause,
}
}
// batchVerifier is a type that implements BatchVerifier and can verify a batch
// from the point of view of the trader.
type batchVerifier struct {
orderStore Store
getAccount func(*btcec.PublicKey) (*account.Account, error)
wallet lndclient.WalletKitClient
ourNodePubkey [33]byte
version BatchVersion
}
// Verify makes sure the batch prepared by the server is correct and can be
// accepted by the trader.
//
// NOTE: This method is part of the BatchVerifier interface.
func (v *batchVerifier) Verify(batch *Batch, bestHeight uint32) error {
// First, make sure the server used the same batch version than we use
// for creating the batch. Otherwise, we bail out of the batch.
// This should already be handled when the client connects/
// authenticates. But doesn't hurt to check again.
if batch.Version != v.version {
return NewErrVersionMismatch(v.version, batch.Version)
}
// Reject the batch if we're too far in the past or future compared to
// the auctioneer.
if bestHeight < batch.HeightHint-heightHintPadding ||
bestHeight > batch.HeightHint+heightHintPadding {
return ErrInvalidBatchHeightHint
}
// First go through all orders that were matched for us. We'll make sure
// we know of the order and that the numbers check out on a high level.
tallies := make(map[[33]byte]*AccountTally)
accounts := make(map[[33]byte]*account.Account)
for nonce, theirOrders := range batch.MatchedOrders {
// Find our order in the database.
ourOrder, err := v.orderStore.GetOrder(nonce)
if err != nil {
return fmt.Errorf("order %v not found: %v", nonce, err)
}
// We'll index our account tallies by the serialized form of
// the account key so some copying is necessary first.
acctKeyRaw := ourOrder.Details().AcctKey
acctKey, err := btcec.ParsePubKey(acctKeyRaw[:])
if err != nil {
return err
}
// Find the account the order spends from, if it isn't already
// in the cache because another order spends from it.
tally, ok := tallies[acctKeyRaw]
if !ok {
acct, err := v.getAccount(acctKey)
if err != nil {
return fmt.Errorf("account %x not found: %v",
acctKeyRaw, err)
}
tally = &AccountTally{
EndingBalance: acct.Value,
}
tallies[acctKeyRaw] = tally
accounts[acctKeyRaw] = acct
}
// The clearing price is different for each duration.
ourOrderDuration := ourOrder.Details().LeaseDuration
clearingPrice := batch.ClearingPrices[ourOrderDuration]
// Now that we know which of our orders were involved in the
// match, we can start validating the match and tally up the
// account balance, executed units and fee diffs.
unitsFilled := SupplyUnit(0)
for _, theirOrder := range theirOrders {
// Verify order compatibility and fee structure.
err = v.validateMatchedOrder(
tally, ourOrder, theirOrder, batch.ExecutionFee,
clearingPrice,
)
if err != nil {
return newMismatchErr(
err, "error matching against order %v",
theirOrder.Order.Nonce(),
)
}
// Make sure there is a channel output included in the
// batch transaction that has the multisig script we
// expect.
err = v.validateChannelOutput(
batch, ourOrder, theirOrder,
)
if err != nil {
return newMismatchErr(
err, "error finding channel output "+
"for matched order %v",
theirOrder.Order.Nonce(),
)
}
// The match looks good, one channel output more to pay
// chain fees for.
tally.NumChansCreated++
unitsFilled += theirOrder.UnitsFilled
}
// Verify the clearing price satisfies our order.
ourOrderPrice := FixedRatePremium(ourOrder.Details().FixedRate)
switch {
// Bids should always have a price greater than or equal to the
// clearing price.
case ourOrder.Type() == TypeBid && ourOrderPrice < clearingPrice:
return &MismatchErr{
msg: fmt.Sprintf("bid order %v has price %v "+
"below clearing price %v", nonce,
ourOrderPrice, clearingPrice),
}
// Asks should always have a price less than or equal to the
// clearing price.
case ourOrder.Type() == TypeAsk && ourOrderPrice > clearingPrice:
return &MismatchErr{
msg: fmt.Sprintf("ask order %v has price %v "+
"above clearing price %v", nonce,
ourOrderPrice, clearingPrice),
}
}
// Last check is to make sure our order has not been over/under
// filled somehow.
switch {
case unitsFilled > ourOrder.Details().UnitsUnfulfilled:
return &MismatchErr{
msg: fmt.Sprintf("invalid units to be filled "+
"for order %v. currently unfulfilled "+
"%d, matched with %d in total",
ourOrder.Nonce(),
ourOrder.Details().UnitsUnfulfilled,
unitsFilled,
),
}
// For the BTCOutboundLiquidity market exactly one unit will be
// matched.
case ourOrder.Details().AuctionType != BTCOutboundLiquidity &&
unitsFilled < ourOrder.Details().MinUnitsMatch:
return &MismatchErr{
msg: fmt.Sprintf("invalid units to be filled "+
"for order %v. matched %d units, but "+
"minimum is %d", ourOrder.Nonce(),
unitsFilled,
ourOrder.Details().MinUnitsMatch),
}
}
}
// Now that we know all the accounts that were involved in the batch,
// we can make sure we got a diff for each of them.
for _, diff := range batch.AccountDiffs {
// We only should get diffs for accounts that have orders in the
// batch. If not, something's messed up.
tally, ok := tallies[diff.AccountKeyRaw]
if !ok {
return &MismatchErr{
msg: fmt.Sprintf("got diff for uninvolved "+
"account %x", diff.AccountKeyRaw),
}
}
acct := accounts[diff.AccountKeyRaw]
// Now that we know how many channels were created from the
// given account, let's also account for the chain fees.
tally.ChainFees(batch.BatchTxFeeRate, acct.Version)
// Even if the account output is dust, we should arrive at the
// same number with our tally as the server.
if diff.EndingBalance != tally.EndingBalance {
return &MismatchErr{
msg: fmt.Sprintf("server sent unexpected "+
"ending balance. got %d expected %d",
diff.EndingBalance, tally.EndingBalance),
}
}
// Update account expiry if needed.
if batch.Version.SupportsAccountExtension() &&
diff.NewExpiry != 0 {
acct.Expiry = diff.NewExpiry
}
// Update account version if needed.
if batch.Version.SupportsAccountTaprootUpgrade() &&
diff.NewVersion > acct.Version {
acct.Version = diff.NewVersion
}
// Make sure the ending state of the account is correct.
err := diff.validateEndingState(batch.BatchTX, acct)
if err != nil {
return newMismatchErr(
err, "account %x diff is incorrect",
diff.AccountKeyRaw,
)
}
}
// From what we can tell, the batch looks good. At least our part checks
// out at this point.
return nil
}
// validateMatchedOrder validates our order against another trader's order and
// tallies up our order's account balance.
func (v *batchVerifier) validateMatchedOrder(tally *AccountTally,
ourOrder Order, otherOrder *MatchedOrder, executionFee terms.FeeSchedule,
clearingPrice FixedRatePremium) error {
// Order type must be opposite.
if otherOrder.Order.Type() == ourOrder.Type() {
return fmt.Errorf("order %v matched same type "+
"orders", ourOrder.Nonce())
}
// Auction types must match.
auctionType := ourOrder.Details().AuctionType
if auctionType != otherOrder.Order.Details().AuctionType {
return fmt.Errorf("order %v did not match the same auction "+
"type", ourOrder.Nonce())
}
// Make sure we weren't matched to our own order.
if otherOrder.NodeKey == v.ourNodePubkey {
return fmt.Errorf("other order is an order from our node")
}
// Verify that the durations overlap. Then tally up all the fees and
// units that were paid/accrued in this matched order pair. We can
// safely cast orders here because we made sure we have the right types
// in the previous step.
switch ours := ourOrder.(type) {
case *Ask:
other := otherOrder.Order.(*Bid)
if other.LeaseDuration != ours.LeaseDuration {
return fmt.Errorf("order duration not overlapping " +
"for our ask")
}
// The ask's price cannot be higher than the bid's price.
if ours.FixedRate > other.FixedRate {
return fmt.Errorf("ask price greater than bid price")
}
makerAmt := otherOrder.UnitsFilled.ToSatoshis()
premiumAmt := makerAmt
if auctionType == BTCOutboundLiquidity {
premiumAmt += other.SelfChanBalance
}
// This match checks out, deduct it from the account's balance.
tally.CalcMakerDelta(
executionFee, clearingPrice, makerAmt, premiumAmt,
other.LeaseDuration,
)
case *Bid:
other := otherOrder.Order.(*Ask)
if other.LeaseDuration != ours.LeaseDuration {
return fmt.Errorf("order duration not overlapping " +
"for our bid")
}
// The ask's price cannot be higher than the bid's price.
if other.FixedRate > ours.FixedRate {
return fmt.Errorf("ask price greater than bid price")
}
takerAmt := ours.SelfChanBalance
premiumAmt := otherOrder.UnitsFilled.ToSatoshis()
if auctionType == BTCOutboundLiquidity {
premiumAmt += takerAmt
}
// This match checks out, deduct it from the account's balance.
tally.CalcTakerDelta(
executionFee, clearingPrice, takerAmt, premiumAmt,
ours.LeaseDuration,
)
}
// Everything checks out so far.
return nil
}
// validateChannelOutput makes sure there is a channel output in the batch TX
// that spends the correct amount for the matched units to the correct multisig
// script that can be used by us to open the channel.
func (v *batchVerifier) validateChannelOutput(batch *Batch, ourOrder Order,
otherOrder *MatchedOrder) error {
_, _, err := ChannelOutput(
batch.BatchTX, v.wallet, ourOrder, otherOrder,
)
return err
}
// A compile-time constraint to ensure batchVerifier implements BatchVerifier.
var _ BatchVerifier = (*batchVerifier)(nil)
// DetermineCommitmentType determines the type of channel to open based on our
// order and the one matched to us and also returns whether that channel type
// uses a Musig2 construction or not.
func DetermineCommitmentType(ourOrder,
theirOrder *Kit) (lnrpc.CommitmentType, bool) {
switch {
// Since everyone needs to be on lnd 0.15.5+ because of the chain sync
// issue, we can safely assume that everyone supports the new script
// enforced type. So if one side indicates they want it, we'll use it.
case ourOrder.ChannelType == ChannelTypeScriptEnforced ||
theirOrder.ChannelType == ChannelTypeScriptEnforced:
return lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, false
// For Simple Taproot channels, both sides need to activate them, so we
// need to make sure both parties explicitly requested them (which is
// gated by the startup feature check).
case ourOrder.ChannelType == ChannelTypeSimpleTaproot &&
theirOrder.ChannelType == ChannelTypeSimpleTaproot:
return lnrpc.CommitmentType_SIMPLE_TAPROOT, true
default:
return lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE, false
}
}