mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-16 13:00:39 +02:00
As a preparation to avoid package import cycles when introducing more generic terms, we move the FeeSchedule interface and its LinearFeeSchedule implementation to the terms package.
320 lines
9.6 KiB
Go
320 lines
9.6 KiB
Go
package order
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/llm/account"
|
|
"github.com/lightninglabs/llm/terms"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
)
|
|
|
|
const (
|
|
// deriveKeyTimeout is the number of seconds we allow the wallet to take
|
|
// to derive a key.
|
|
deriveKeyTimeout = 10 * time.Second
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 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) error {
|
|
// First of all, make sure we're using the same batch validation version
|
|
// as the server. 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 != CurrentVersion {
|
|
return ErrVersionMismatch
|
|
}
|
|
|
|
// 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[:], btcec.S256())
|
|
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
|
|
}
|
|
|
|
// 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,
|
|
batch.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.BatchTX, 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
|
|
}
|
|
|
|
// Last check is to make sure our order has not been over
|
|
// filled somehow.
|
|
if 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,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 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),
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
|
|
// 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.MinDuration > ours.MaxDuration {
|
|
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")
|
|
}
|
|
|
|
// This match checks out, deduct it from the account's balance.
|
|
tally.CalcMakerDelta(
|
|
executionFee, clearingPrice,
|
|
otherOrder.UnitsFilled.ToSatoshis(), other.MinDuration,
|
|
)
|
|
|
|
case *Bid:
|
|
other := otherOrder.Order.(*Ask)
|
|
if other.MaxDuration < ours.MinDuration {
|
|
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")
|
|
}
|
|
|
|
// This match checks out, deduct it from the account's balance.
|
|
tally.CalcTakerDelta(
|
|
executionFee, clearingPrice,
|
|
otherOrder.UnitsFilled.ToSatoshis(), ours.MinDuration,
|
|
)
|
|
}
|
|
|
|
// 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(batchTx *wire.MsgTx,
|
|
ourOrder Order, otherOrder *MatchedOrder) error {
|
|
|
|
// Re-derive our multisig key first.
|
|
ctxt, cancel := context.WithTimeout(
|
|
context.Background(), deriveKeyTimeout,
|
|
)
|
|
defer cancel()
|
|
ourKey, err := v.wallet.DeriveKey(
|
|
ctxt, &ourOrder.Details().MultiSigKeyLocator,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not derive our multisig key: %v", err)
|
|
}
|
|
|
|
// Gather the information we expect to find in the batch TX.
|
|
expectedOutputSize := otherOrder.UnitsFilled.ToSatoshis()
|
|
_, expectedOut, err := input.GenFundingPkScript(
|
|
ourKey.PubKey.SerializeCompressed(), otherOrder.MultiSigKey[:],
|
|
int64(expectedOutputSize),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create multisig script: %v", err)
|
|
}
|
|
|
|
// Locate the channel output now that we know what to look for.
|
|
for _, out := range batchTx.TxOut {
|
|
if out.Value == expectedOut.Value &&
|
|
bytes.Equal(out.PkScript, expectedOut.PkScript) {
|
|
|
|
// Bingo, this is what we want.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no channel output found in batch tx for matched "+
|
|
"order %v", otherOrder.Order.Nonce())
|
|
}
|
|
|
|
// A compile-time constraint to ensure batchVerifier implements BatchVerifier.
|
|
var _ BatchVerifier = (*batchVerifier)(nil)
|