pool/order/interfaces_test.go

424 lines
10 KiB
Go

package order
import (
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/terms"
"github.com/stretchr/testify/require"
)
// TestOrderReservedValue checks orders' ReservedValue method returning the
// expected worst case value.
func TestOrderReservedValue(t *testing.T) {
t.Parallel()
simpleFeeSchedule := terms.NewLinearFeeSchedule(1, 100)
type testCase struct {
name string
order Order
}
testCases := []*testCase{
{
name: "bid 1 unit",
order: &Bid{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 1,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask 1 unit",
order: &Ask{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 1,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "bid 10 units",
order: &Bid{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask 10 units",
order: &Ask{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "cancelled order",
order: &Ask{
Kit: Kit{
State: StateCanceled,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "expired order",
order: &Bid{
Kit: Kit{
State: StateExpired,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "failed order",
order: &Bid{
Kit: Kit{
State: StateFailed,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask 10 units partially filled",
order: &Ask{
Kit: Kit{
State: StatePartiallyFilled,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask 10 units cleared",
order: &Ask{
Kit: Kit{
State: StateCleared,
UnitsUnfulfilled: 10,
FixedRate: 10000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask massive rate",
order: &Ask{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10_000_000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 1,
},
},
},
{
name: "ask 10 units 5 min units match",
order: &Ask{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10_000_000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 5,
},
},
},
{
name: "bid 10 units 5 min units match",
order: &Bid{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10_000_000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 5,
},
},
},
{
name: "ask 10 units 4 min units match",
order: &Ask{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10_000_000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 4,
},
},
},
{
name: "bid 10 units 4 min units match",
order: &Bid{
Kit: Kit{
State: StateSubmitted,
UnitsUnfulfilled: 10,
FixedRate: 10_000_000,
MaxBatchFeeRate: 1000,
LeaseDuration: 144,
MinUnitsMatch: 4,
},
},
},
}
runTestCase := func(t *testing.T, tc *testCase, v account.Version) {
// Count the worst case we will expect.
var expValue btcutil.Amount
switch o := tc.order.(type) {
case *Bid:
// Expect no reseved value in these states.
if o.State.Archived() {
break
}
// For bids the taker pays the most fees if the min
// units get matched every block. There's an edge case
// where if the units unfulfilled is not divisible by
// the min units match, then the last match will consume
// all the remaining units left.
numBlocks := int(o.UnitsUnfulfilled / o.MinUnitsMatch)
unitsRem := o.UnitsUnfulfilled % o.MinUnitsMatch
lastMatch := o.MinUnitsMatch
if unitsRem != 0 {
lastMatch += unitsRem
}
for i := 0; i < numBlocks; i++ {
amt := o.MinUnitsMatch.ToSatoshis()
if i == numBlocks-1 {
amt = lastMatch.ToSatoshis()
}
lumpSum := FixedRatePremium(o.FixedRate).
LumpSumPremium(amt, o.LeaseDuration)
exeFee := executionFee(amt, simpleFeeSchedule)
chainFee := EstimateTraderFee(
1, o.MaxBatchFeeRate, v,
)
// For bids the lump sum, chain fee and the
// execution fee must be reserved.
expValue += lumpSum + chainFee + exeFee
}
case *Ask:
// Expect no reseved value in these states.
if o.State.Archived() {
break
}
// For asks the maker pays the most fees if min units
// get matched every block. There's an edge case where
// if the units unfulfilled is not divisible by the min
// units match, then the last match will consume all the
// remaining units left.
numBlocks := int(o.UnitsUnfulfilled / o.MinUnitsMatch)
unitsRem := o.UnitsUnfulfilled % o.MinUnitsMatch
lastMatch := o.MinUnitsMatch
if unitsRem != 0 {
lastMatch += unitsRem
}
for i := 0; i < numBlocks; i++ {
amt := o.MinUnitsMatch.ToSatoshis()
if i == numBlocks-1 {
amt = lastMatch.ToSatoshis()
}
// In the worst case, the maker will be paid
// only one lump sum for a 144 block duration,
// since that is the minimum duration.
lumpSum := FixedRatePremium(o.FixedRate).
LumpSumPremium(amt, 144)
exeFee := executionFee(amt, simpleFeeSchedule)
chainFee := EstimateTraderFee(
1, o.MaxBatchFeeRate, v,
)
// For asks the amount itself, the chain fee
// and the execution fee must be reserved,
// while the lump sum the maker gets back.
expValue += amt + chainFee + exeFee - lumpSum
}
default:
t.Fatalf("unknown type %T", tc.order)
}
// We don't ever expect negative reserved values.
if expValue < 0 {
expValue = 0
}
val := tc.order.ReservedValue(simpleFeeSchedule, v)
if val < 0 {
t.Fatalf("reserved value cannot be "+
"negative: %v", val)
}
if val != expValue {
t.Fatalf("%s: expected reserved value "+
"%v, got '%v'", tc.name, expValue, val)
}
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name+"/version_0", func(t *testing.T) {
runTestCase(t, tc, account.VersionInitialNoVersion)
})
t.Run(tc.name+"/version_1", func(t *testing.T) {
runTestCase(t, tc, account.VersionTaprootEnabled)
})
t.Run(tc.name+"/version_2", func(t *testing.T) {
runTestCase(t, tc, account.VersionMuSig2V100RC2)
})
}
}
var channelAnnouncementConstrainsTestCases = []struct {
name string
askerConstrains ChannelAnnouncementConstraints
unannouncedChannel bool
result bool
}{{
name: "ask no preference bid announced channel",
askerConstrains: AnnouncementNoPreference,
unannouncedChannel: false,
result: true,
}, {
name: "ask no preference bid unannounced channel",
askerConstrains: AnnouncementNoPreference,
unannouncedChannel: true,
result: true,
}, {
name: "ask only announced bid announced channel",
askerConstrains: OnlyAnnounced,
unannouncedChannel: false,
result: true,
}, {
name: "ask only announced bid unannounced channel",
askerConstrains: OnlyAnnounced,
unannouncedChannel: true,
result: false,
}, {
name: "ask only unannounced bid announced channel",
askerConstrains: OnlyUnannounced,
unannouncedChannel: false,
result: false,
}, {
name: "ask only unannounced bid unannounced channel",
askerConstrains: OnlyUnannounced,
unannouncedChannel: true,
result: true,
}}
func TestChannelAnnouncementConstrainsCompatibility(t *testing.T) {
for _, tc := range channelAnnouncementConstrainsTestCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
res := MatchAnnouncementConstraints(
tc.askerConstrains, tc.unannouncedChannel,
)
require.Equal(t, tc.result, res)
})
}
}
var channelConfirmationConstrainsTestCases = []struct {
name string
askerConstrains ChannelConfirmationConstraints
zeroConfChannel bool
result bool
}{{
name: "ask no preference bid zero confirmed channel",
askerConstrains: ConfirmationNoPreference,
zeroConfChannel: false,
result: true,
}, {
name: "ask no preference bid zero conf channel",
askerConstrains: ConfirmationNoPreference,
zeroConfChannel: true,
result: true,
}, {
name: "ask only confirmed channels bid confirmed channel",
askerConstrains: OnlyConfirmed,
zeroConfChannel: false,
result: true,
}, {
name: "ask only confirmed bid zero conf channel",
askerConstrains: OnlyConfirmed,
zeroConfChannel: true,
result: false,
}, {
name: "ask only zero conf channels bid confirmed channel",
askerConstrains: OnlyZeroConf,
zeroConfChannel: false,
result: false,
}, {
name: "ask only zero conf channels bid zero conf channel",
askerConstrains: OnlyZeroConf,
zeroConfChannel: true,
result: true,
}}
func TestChannelConstrainsCompatibility(t *testing.T) {
for _, tc := range channelConfirmationConstrainsTestCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
res := MatchZeroConfConstraints(
tc.askerConstrains, tc.zeroConfChannel,
)
require.Equal(t, tc.result, res)
})
}
}