multi: update order/batch verification for outbound liquidity auctions

This commit is contained in:
positiveblue 2022-09-22 07:36:06 -07:00
parent 34bbd1ec3f
commit e5d78fd3cb
No known key found for this signature in database
GPG key ID: 4FFF2510928804DC
11 changed files with 207 additions and 81 deletions

View file

@ -721,8 +721,9 @@ func parseBaseBid(ctx *cli.Context) (*poolrpc.Bid, *sidecar.Ticket, error) {
if ctx.IsSet("self_chan_balance") {
bid.SelfChanBalance = ctx.Uint64("self_chan_balance")
bidAmt := btcutil.Amount(bid.Details.Amt)
err := sidecar.CheckOfferParams(
bidAmt, btcutil.Amount(bid.SelfChanBalance),
err := order.CheckOfferParams(
order.AuctionType(bid.Details.AuctionType), bidAmt,
btcutil.Amount(bid.SelfChanBalance),
order.BaseSupplyUnit,
)
if err != nil {

View file

@ -1023,8 +1023,15 @@ func (m *Manager) OfferSidecar(ctx context.Context, capacity,
acctPubKey *keychain.KeyDescriptor,
bid *order.Bid, auto bool) (*sidecar.Ticket, error) {
if bid.Details().AuctionType != order.BTCInboundLiquidity {
return nil, fmt.Errorf("%s market does not support sidecar "+
"tickets", bid.Details().AuctionType)
}
// Make sure the capacity and push amounts are sane.
err := sidecar.CheckOfferParams(capacity, pushAmt, order.BaseSupplyUnit)
err := order.CheckOfferParams(
bid.AuctionType, capacity, pushAmt, order.BaseSupplyUnit,
)
if err != nil {
return nil, err
}

View file

@ -805,28 +805,66 @@ func TestOfferSidecarValidation(t *testing.T) {
name string
capacity btcutil.Amount
pushAmt btcutil.Amount
bid *order.Bid
expectedErr string
}{{
name: "empty capacity",
bid: &order.Bid{},
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid capacity",
capacity: 123,
bid: &order.Bid{},
expectedErr: "channel capacity must be positive multiple of",
}, {
name: "invalid push amount",
capacity: 100000,
pushAmt: 100001,
bid: &order.Bid{},
expectedErr: "self channel balance must be smaller than " +
"or equal to capacity",
}, {
name: "no sidecar for outbound market",
capacity: 100000,
pushAmt: 0,
bid: &order.Bid{
Kit: order.Kit{
AuctionType: order.BTCOutboundLiquidity,
},
},
expectedErr: "market does not support sidecar tickets",
}}
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
desc := &keychain.KeyDescriptor{
PubKey: privKey.PubKey(),
}
// We'll need a formally valid signature to pass the parsing. So we'll
// just create a dummy signature from a random key pair.
hash := sha256.New()
_, _ = hash.Write([]byte("foo"))
digest := hash.Sum(nil)
sig := ecdsa.Sign(privKey, digest)
h.mgr.cfg.NodePubKey = privKey.PubKey()
h.signerMock.Signature = sig.Serialize()
var nodeKeyRaw [33]byte
copy(nodeKeyRaw[:], privKey.PubKey().SerializeCompressed())
for _, testCase := range negativeCases {
_, err := h.mgr.OfferSidecar(
context.Background(), testCase.capacity,
testCase.pushAmt, 2016, nil, nil, false,
testCase.pushAmt, 2016, desc, testCase.bid, false,
)
require.Error(t, err)
require.Contains(t, err.Error(), testCase.expectedErr)
fmt.Println(testCase.expectedErr)
if testCase.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), testCase.expectedErr)
continue
}
require.NoError(t, err)
}
}

View file

@ -203,7 +203,11 @@ func (v *batchVerifier) Verify(batch *Batch, bestHeight uint32) error {
),
}
case unitsFilled < ourOrder.Details().MinUnitsMatch:
// 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 "+
@ -283,6 +287,13 @@ func (v *batchVerifier) validateMatchedOrder(tally *AccountTally,
"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")
@ -305,10 +316,15 @@ func (v *batchVerifier) validateMatchedOrder(tally *AccountTally,
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,
otherOrder.UnitsFilled.ToSatoshis(),
executionFee, clearingPrice, makerAmt, premiumAmt,
other.LeaseDuration,
)
@ -324,11 +340,16 @@ func (v *batchVerifier) validateMatchedOrder(tally *AccountTally,
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,
otherOrder.UnitsFilled.ToSatoshis(),
ours.SelfChanBalance, ours.LeaseDuration,
executionFee, clearingPrice, takerAmt, premiumAmt,
ours.LeaseDuration,
)
}

View file

@ -358,6 +358,40 @@ func TestBatchVerifier(t *testing.T) {
return v.Verify(b, bestHeight)
},
},
{
name: "auction type mismatch",
expectedErr: "did not match the same auction type",
doVerify: func(v BatchVerifier, a *Ask, b1, b2 *Bid,
b *Batch) error {
a.Details().AuctionType = BTCInboundLiquidity
b1.Details().AuctionType = BTCOutboundLiquidity
return v.Verify(b, bestHeight)
},
},
{
// TODO(positiveblue): make a more specific test for
// btc outbound liquidity market.
name: "premiums are based on aucton type",
expectedErr: "ending balance 934",
doVerify: func(v BatchVerifier, a *Ask, b1, b2 *Bid,
b *Batch) error {
a.Details().AuctionType = BTCOutboundLiquidity
b1.Details().AuctionType = BTCOutboundLiquidity
b2.Details().AuctionType = BTCOutboundLiquidity
b.AccountDiffs[0].EndingBalance = 395_089
b.BatchTX.TxOut[2].Value = 395_089
// NOTE: the ask and bids would not be valid
// orders for the outbound liquidity market
// because they bigger than one unit but we are
// testing that premiums are calculated
// properly here.
b.AccountDiffs[1].EndingBalance = 934
return v.Verify(b, bestHeight)
},
},
}
// Run through all the test cases, creating a new, valid batch each
@ -430,7 +464,7 @@ func TestBatchVerifier(t *testing.T) {
// 2000 * (200_000 * 5000 / 1_000_000_000) = 1000 sats premium
LeaseDuration: leaseDuration,
}),
SelfChanBalance: 50,
SelfChanBalance: 100_000,
}
pkScript := scriptForAcct(t, bigAcct)
// If account extension is supported we create the pkScript with
@ -456,7 +490,7 @@ func TestBatchVerifier(t *testing.T) {
// Channel output for channel between ask and
// bid2.
{
Value: 200_050,
Value: 300_000,
PkScript: scriptForChan(
t, walletKit,
ask.MultiSigKeyLocator,
@ -469,8 +503,8 @@ func TestBatchVerifier(t *testing.T) {
// bid1ExecFee - bid2ExecFee - chainFees
// - bid2SelfChanBalance
// 500_000 - 1000 - 1000 -
// 1_110 - 1_110 - 186 - 50
Value: 495_544,
// 1_110 - 1_110 - 186 - 100_000
Value: 395_594,
PkScript: pkScript,
},
},
@ -485,7 +519,7 @@ func TestBatchVerifier(t *testing.T) {
AccountKey: acctKeyBig,
EndingState: stateRecreated,
OutpointIndex: 2,
EndingBalance: 495_544,
EndingBalance: 395_594,
},
{
AccountKeyRaw: acctIDSmall,

View file

@ -639,7 +639,7 @@ func (a *Ask) ReservedValue(feeSchedule terms.FeeSchedule,
return reservedValue(a, func(amt btcutil.Amount) btcutil.Amount {
delta, _, _ := makerDelta(
feeSchedule, clearingPrice, amt, a.LeaseDuration,
feeSchedule, clearingPrice, amt, amt, a.LeaseDuration,
)
return delta
}, accountVersion)
@ -823,14 +823,77 @@ func (b *Bid) ReservedValue(feeSchedule terms.FeeSchedule,
clearingPrice := FixedRatePremium(b.FixedRate)
return reservedValue(b, func(amt btcutil.Amount) btcutil.Amount {
premiumAmt := amt
if b.Details().AuctionType == BTCOutboundLiquidity {
premiumAmt += b.SelfChanBalance
}
delta, _, _ := takerDelta(
feeSchedule, clearingPrice, amt, b.SelfChanBalance,
b.LeaseDuration,
feeSchedule, clearingPrice, premiumAmt,
b.SelfChanBalance, b.LeaseDuration,
)
return delta
}, accountVersion)
}
// CheckOfferParams makes sure the offer parameters of an offer are valid and
// sane.
func CheckOfferParams(auctionType AuctionType, capacity, pushAmt,
baseSupplyUnit btcutil.Amount) error {
if capacity == 0 || capacity%baseSupplyUnit != 0 {
return fmt.Errorf("channel capacity must be positive multiple "+
"of %d", baseSupplyUnit)
}
if auctionType == BTCInboundLiquidity && pushAmt > capacity {
return fmt.Errorf("self channel balance must be smaller than " +
"or equal to capacity")
}
if auctionType == BTCOutboundLiquidity {
// Only multiples of 100k sats are allowed in the outbound
// market.
if pushAmt == 0 || pushAmt%baseSupplyUnit != 0 {
return fmt.Errorf("self balance must be a positive "+
"multiple of %d", baseSupplyUnit)
}
}
return nil
}
// CheckOfferParamsForOrder makes sure that the order parameters in a
// sidecar offer are formally valid, sane and match the order parameters.
func CheckOfferParamsForOrder(auctionType AuctionType, offer sidecar.Offer,
bidAmt, bidMinUnitsMatch, baseSupplyUnit btcutil.Amount) error {
if auctionType != BTCInboundLiquidity {
return fmt.Errorf("%s market does not support sidecar tickets",
auctionType)
}
err := CheckOfferParams(
auctionType, offer.Capacity, offer.PushAmt, baseSupplyUnit,
)
if err != nil {
return err
}
if offer.Capacity != bidAmt {
return fmt.Errorf("invalid bid amount %v, must match sidecar "+
"ticket's capacity %v", bidAmt, offer.Capacity)
}
if offer.Capacity != bidMinUnitsMatch*baseSupplyUnit {
return fmt.Errorf("invalid min units match %v, must match "+
"sidecar ticket's capacity %v",
bidMinUnitsMatch*baseSupplyUnit, offer.Capacity)
}
return nil
}
// ValidateSelfChanBalance makes sure that all conditions to use the
// SelfChanBalance field on a bid order are met.
func (b *Bid) ValidateSelfChanBalance() error {
@ -839,8 +902,8 @@ func (b *Bid) ValidateSelfChanBalance() error {
"order version")
}
if err := sidecar.CheckOfferParams(
b.Amt, b.SelfChanBalance, BaseSupplyUnit,
if err := CheckOfferParams(
b.AuctionType, b.Amt, b.SelfChanBalance, BaseSupplyUnit,
); err != nil {
return fmt.Errorf("invalid self chan balance: %v", err)
}

View file

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
// TestOrderReservedValue checks orders' ReservedValue merhod returning the
// TestOrderReservedValue checks orders' ReservedValue method returning the
// expected worst case value.
func TestOrderReservedValue(t *testing.T) {
t.Parallel()
@ -240,7 +240,7 @@ func TestOrderReservedValue(t *testing.T) {
1, o.MaxBatchFeeRate, v,
)
// For bids the lump sum, chain fee and the
// For bids the lump sum, chain fee and the
// execution fee must be reserved.
expValue += lumpSum + chainFee + exeFee
}

View file

@ -465,8 +465,9 @@ func (m *manager) validateAndSignTicketForOrder(ctx context.Context,
// The signature is valid! Let's now make sure the offer and the order
// parameters actually match.
err := sidecar.CheckOfferParamsForOrder(
o, bid.Amt, btcutil.Amount(bid.MinUnitsMatch), BaseSupplyUnit,
err := CheckOfferParamsForOrder(
bid.AuctionType, o, bid.Amt, btcutil.Amount(bid.MinUnitsMatch),
BaseSupplyUnit,
)
if err != nil {
return err

View file

@ -90,7 +90,9 @@ func ParseRPCOrder(version, leaseDuration uint32,
return nil, errors.New("min units match must be greater than 0")
// The min units match must not exceed the total order units.
case details.MinUnitsMatch > uint32(kit.Units):
case kit.AuctionType != BTCOutboundLiquidity &&
details.MinUnitsMatch > uint32(kit.Units):
return nil, errors.New("min units match must not exceed " +
"total order units")
}

View file

@ -174,23 +174,23 @@ type AccountTally struct {
// order where the account is involved on the maker side. It returns the
// balance delta, fees accrued, and execution fee paid.
func makerDelta(feeSchedule terms.FeeSchedule, price FixedRatePremium,
totalSats btcutil.Amount, duration uint32) (btcutil.Amount,
makerAmt, baseAmt btcutil.Amount, duration uint32) (btcutil.Amount,
btcutil.Amount, btcutil.Amount) {
// First, we'll need to subtract the total amount of sats cleared (the
// First, we'll need to subtract the total matched amount (the
// channel size), from the balance of the maker.
balanceDelta := -totalSats
balanceDelta := -makerAmt
// Calculate the premium based on the duration the capital will be
// locked for.
satsPremium := price.LumpSumPremium(totalSats, duration)
satsPremium := price.LumpSumPremium(baseAmt, duration)
// The premium is paid to the maker.
balanceDelta += satsPremium
makerFeesAccrued := satsPremium
// Finally, we'll subtract the paid execution fees from the balance.
executionFee := executionFee(totalSats, feeSchedule)
executionFee := executionFee(makerAmt, feeSchedule)
balanceDelta -= executionFee
return balanceDelta, makerFeesAccrued, executionFee
@ -200,12 +200,12 @@ func makerDelta(feeSchedule terms.FeeSchedule, price FixedRatePremium,
// single order where the account is involved on the maker side. It returns the
// execution fee that is collected by the auctioneer.
func (t *AccountTally) CalcMakerDelta(feeSchedule terms.FeeSchedule,
price FixedRatePremium, totalSats btcutil.Amount,
price FixedRatePremium, makerAmt, premiumAmt btcutil.Amount,
duration uint32) btcutil.Amount {
// Calculate the deltas and update the tally.
balanceDelta, makerFeesAccrued, executionFee := makerDelta(
feeSchedule, price, totalSats, duration,
feeSchedule, price, makerAmt, premiumAmt, duration,
)
t.EndingBalance += balanceDelta
@ -219,12 +219,12 @@ func (t *AccountTally) CalcMakerDelta(feeSchedule terms.FeeSchedule,
// order where the account is involved on the taker side. It returns the
// balance delta, taker fees paid, and execution fee paid.
func takerDelta(feeSchedule terms.FeeSchedule,
price FixedRatePremium, totalSats, selfChanBalance btcutil.Amount,
price FixedRatePremium, baseAmt, takerAmt btcutil.Amount,
duration uint32) (btcutil.Amount, btcutil.Amount, btcutil.Amount) {
// Calculate the premium based on the duration the capital will be
// locked for.
satsPremium := price.LumpSumPremium(totalSats, duration)
satsPremium := price.LumpSumPremium(baseAmt, duration)
// The premium must be paid by the taker.
balanceDelta := -satsPremium
@ -232,10 +232,10 @@ func takerDelta(feeSchedule terms.FeeSchedule,
// If there is a self channel balance, it must also be paid out of the
// taker account.
balanceDelta -= selfChanBalance
balanceDelta -= takerAmt
// Finally, we'll subtract the paid execution fees from the balance.
executionFee := executionFee(totalSats, feeSchedule)
executionFee := executionFee(baseAmt, feeSchedule)
balanceDelta -= executionFee
return balanceDelta, takerFeesPaid, executionFee
@ -245,12 +245,12 @@ func takerDelta(feeSchedule terms.FeeSchedule,
// single order where the account is involved on the taker side. It returns the
// execution fee that is collected by the auctioneer.
func (t *AccountTally) CalcTakerDelta(feeSchedule terms.FeeSchedule,
price FixedRatePremium, totalSats, selfChanBalance btcutil.Amount,
price FixedRatePremium, takerAmt, premiumAmt btcutil.Amount,
duration uint32) btcutil.Amount {
// Calculate the deltas and update the tally.
balanceDelta, takerFeesPaid, executionFee := takerDelta(
feeSchedule, price, totalSats, selfChanBalance, duration,
feeSchedule, price, premiumAmt, takerAmt, duration,
)
t.EndingBalance += balanceDelta

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
@ -184,43 +183,3 @@ func VerifyOrder(ctx context.Context, ticket *Ticket,
return nil
}
// CheckOfferParams makes sure the offer parameters of a sidecar ticket are
// valid and sane.
func CheckOfferParams(capacity, pushAmt, baseSupplyUnit btcutil.Amount) error {
if capacity == 0 || capacity%baseSupplyUnit != 0 {
return fmt.Errorf("channel capacity must be positive multiple "+
"of %d", baseSupplyUnit)
}
if pushAmt > capacity {
return fmt.Errorf("self channel balance must be smaller than " +
"or equal to capacity")
}
return nil
}
// CheckOfferParamsForOrder makes sure that the order parameters in a
// sidecar offer are formally valid, sane and match the order parameters.
func CheckOfferParamsForOrder(offer Offer, bidAmt, bidMinUnitsMatch,
baseSupplyUnit btcutil.Amount) error {
err := CheckOfferParams(offer.Capacity, offer.PushAmt, baseSupplyUnit)
if err != nil {
return err
}
if offer.Capacity != bidAmt {
return fmt.Errorf("invalid bid amount %v, must match sidecar "+
"ticket's capacity %v", bidAmt, offer.Capacity)
}
if offer.Capacity != bidMinUnitsMatch*baseSupplyUnit {
return fmt.Errorf("invalid min units match %v, must match "+
"sidecar ticket's capacity %v",
bidMinUnitsMatch*baseSupplyUnit, offer.Capacity)
}
return nil
}