mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
402 lines
12 KiB
Go
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
|
|
}
|
|
}
|