pool/order/batch_verifier.go
2023-10-25 10:04:23 +02:00

402 lines
12 KiB
Go

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
}
}