mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
401 lines
9.9 KiB
Go
401 lines
9.9 KiB
Go
package order
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightninglabs/pool/account"
|
|
"github.com/lightninglabs/pool/auctioneerrpc"
|
|
"github.com/lightninglabs/pool/internal/test"
|
|
"github.com/lightninglabs/pool/sidecar"
|
|
"github.com/lightninglabs/pool/terms"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestValidateOrderAccountIsolation tests that when we go to validate that an
|
|
// account has sufficient balance for an order, we only factor in the
|
|
// outstanding orders for that account, and not all total orders that are open.
|
|
func TestValidateOrderAccountIsolation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
orderStore := newMockStore()
|
|
orderManager := NewManager(&ManagerConfig{
|
|
Store: orderStore,
|
|
BatchVersion: LatestBatchVersion,
|
|
})
|
|
|
|
// We'll now create two accounts, one that's 1 BTC in size, while the
|
|
// other is 2 BTC.
|
|
accountA := account.Account{
|
|
Value: btcutil.SatoshiPerBitcoin,
|
|
TraderKey: &keychain.KeyDescriptor{
|
|
PubKey: acctKeySmall,
|
|
},
|
|
}
|
|
accountB := account.Account{
|
|
Value: btcutil.SatoshiPerBitcoin * 2,
|
|
TraderKey: &keychain.KeyDescriptor{
|
|
PubKey: acctKeyBig,
|
|
},
|
|
}
|
|
|
|
// We'll now create an order that will consume at least half (1100
|
|
// units is 1.1 BTC) of account B (it's an ask order).
|
|
var acctKeyB [33]byte
|
|
copy(acctKeyB[:], acctKeyBig.SerializeCompressed())
|
|
orderB := &Ask{
|
|
Kit: newKitFromTemplate(Nonce{0x01}, &Kit{
|
|
State: StateSubmitted,
|
|
UnitsUnfulfilled: 1100,
|
|
FixedRate: 2_000,
|
|
MaxBatchFeeRate: 1000,
|
|
AcctKey: acctKeyB,
|
|
LeaseDuration: 144,
|
|
MinUnitsMatch: 100,
|
|
}),
|
|
}
|
|
|
|
testTerms := &terms.AuctioneerTerms{
|
|
OrderExecBaseFee: 1,
|
|
OrderExecFeeRate: 100,
|
|
LeaseDurationBuckets: map[uint32]auctioneerrpc.DurationBucketState{
|
|
144: auctioneerrpc.DurationBucketState_MARKET_OPEN,
|
|
},
|
|
}
|
|
|
|
// Submitting this order for account B should pass validation.
|
|
err := orderManager.validateOrder(orderB, &accountB, testTerms)
|
|
if err != nil {
|
|
t.Fatalf("order B validation failed: %v", err)
|
|
}
|
|
|
|
// Simulating order acceptance, we'll now add this order to the order
|
|
// store.
|
|
if err := orderStore.SubmitOrder(orderB); err != nil {
|
|
t.Fatalf("unable to submit order: %v", err)
|
|
}
|
|
|
|
// Next, we'll create a ask that'll consume _most_ (but not all) of account
|
|
// A's balance. This should pass validation as although account B can't
|
|
// handle the order account A can.
|
|
var acctKeyA [33]byte
|
|
copy(acctKeyA[:], acctKeySmall.SerializeCompressed())
|
|
orderA := &Ask{
|
|
Kit: newKitFromTemplate(Nonce{0x01}, &Kit{
|
|
State: StateSubmitted,
|
|
UnitsUnfulfilled: 800,
|
|
FixedRate: 2_000,
|
|
MaxBatchFeeRate: 1000,
|
|
AcctKey: acctKeyA,
|
|
LeaseDuration: 144,
|
|
MinUnitsMatch: 100,
|
|
}),
|
|
}
|
|
|
|
err = orderManager.validateOrder(orderA, &accountA, testTerms)
|
|
if err != nil {
|
|
t.Fatalf("order A failed validation: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateOrder makes sure that the order manager properly validates
|
|
// all conditions for submitting orders, especially the conditions for using the
|
|
// self channel balance.
|
|
func TestValidateOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
orderStore := newMockStore()
|
|
orderManager := NewManager(&ManagerConfig{
|
|
Store: orderStore,
|
|
BatchVersion: LatestBatchVersion,
|
|
})
|
|
|
|
// We'll now create an account with sufficient size.
|
|
testAccount := &account.Account{
|
|
Value: btcutil.SatoshiPerBitcoin,
|
|
TraderKey: &keychain.KeyDescriptor{
|
|
PubKey: acctKeySmall,
|
|
},
|
|
}
|
|
|
|
testTerms := &terms.AuctioneerTerms{
|
|
OrderExecBaseFee: 1,
|
|
OrderExecFeeRate: 100,
|
|
LeaseDurationBuckets: map[uint32]auctioneerrpc.DurationBucketState{
|
|
144: auctioneerrpc.DurationBucketState_MARKET_OPEN,
|
|
},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
expectedErr string
|
|
order Order
|
|
}{{
|
|
name: "invalid lease duration",
|
|
expectedErr: "invalid lease duration, must be one of map[144",
|
|
order: &Ask{
|
|
Kit: Kit{
|
|
LeaseDuration: 123,
|
|
},
|
|
},
|
|
}, {
|
|
name: "invalid max batch fee rate",
|
|
expectedErr: "invalid max batch fee rate 123 sat/kw, must be",
|
|
order: &Ask{
|
|
Kit: Kit{
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 123,
|
|
},
|
|
},
|
|
}, {
|
|
name: "insufficient account balance",
|
|
expectedErr: "insufficient account balance",
|
|
order: &Ask{
|
|
Kit: Kit{
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: btcutil.SatoshiPerBitcoin,
|
|
UnitsUnfulfilled: 1_000,
|
|
},
|
|
},
|
|
}, {
|
|
name: "invalid version for self chan balance",
|
|
expectedErr: "cannot use self chan balance with old order " +
|
|
"version",
|
|
order: &Bid{
|
|
Kit: Kit{
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: 100_000,
|
|
UnitsUnfulfilled: 1,
|
|
},
|
|
SelfChanBalance: 1,
|
|
},
|
|
}, {
|
|
name: "invalid capacity for self chan balance",
|
|
expectedErr: "channel capacity must be positive multiple of",
|
|
order: &Bid{
|
|
Kit: Kit{
|
|
Version: VersionSelfChanBalance,
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: 0,
|
|
UnitsUnfulfilled: 0,
|
|
},
|
|
SelfChanBalance: 1,
|
|
},
|
|
}, {
|
|
name: "invalid self chan balance",
|
|
expectedErr: "self channel balance must be smaller than " +
|
|
"or equal to capacity",
|
|
order: &Bid{
|
|
Kit: Kit{
|
|
Version: VersionSelfChanBalance,
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: 100_000,
|
|
UnitsUnfulfilled: 1,
|
|
},
|
|
SelfChanBalance: 100_001,
|
|
},
|
|
}, {
|
|
name: "min units match must equal amount",
|
|
expectedErr: "to use self chan balance the min units match " +
|
|
"must be equal to the order amount in units",
|
|
order: &Bid{
|
|
Kit: Kit{
|
|
Version: VersionSelfChanBalance,
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: 200_000,
|
|
UnitsUnfulfilled: 2,
|
|
},
|
|
SelfChanBalance: 50_000,
|
|
},
|
|
}, {
|
|
name: "happy path",
|
|
expectedErr: "",
|
|
order: &Bid{
|
|
Kit: Kit{
|
|
Version: VersionSelfChanBalance,
|
|
LeaseDuration: 144,
|
|
MaxBatchFeeRate: 253,
|
|
MinUnitsMatch: 1,
|
|
Amt: 100_000,
|
|
UnitsUnfulfilled: 1,
|
|
Units: 1,
|
|
},
|
|
SelfChanBalance: 10_000,
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := orderManager.validateOrder(
|
|
tc.order, testAccount, testTerms,
|
|
)
|
|
|
|
if tc.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockAccountStore struct {
|
|
account.Store
|
|
}
|
|
|
|
// TestPrepareOrderSidecarTicket makes sure that a bid order with a sidecar
|
|
// ticket can be submitted and verified successfully.
|
|
func TestPrepareOrderSidecarTicket(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mockLightning := test.NewMockLightning()
|
|
mockSigner := test.NewMockSigner()
|
|
mgr := NewManager(&ManagerConfig{
|
|
AcctStore: &mockAccountStore{},
|
|
Wallet: test.NewMockWalletKit(),
|
|
Lightning: mockLightning,
|
|
Signer: mockSigner,
|
|
BatchVersion: LatestBatchVersion,
|
|
})
|
|
require.NoError(t, mgr.Start())
|
|
defer mgr.Stop()
|
|
|
|
acct := &account.Account{
|
|
Value: btcutil.SatoshiPerBitcoin,
|
|
TraderKey: &keychain.KeyDescriptor{
|
|
PubKey: acctKeySmall,
|
|
},
|
|
}
|
|
var acctKey [33]byte
|
|
copy(acctKey[:], acct.TraderKey.PubKey.SerializeCompressed())
|
|
bid := &Bid{
|
|
Kit: newKitFromTemplate(Nonce{0x01}, &Kit{
|
|
Amt: 5_000_000,
|
|
State: StateSubmitted,
|
|
AcctKey: acctKey,
|
|
MaxBatchFeeRate: 1000,
|
|
LeaseDuration: 2016,
|
|
MinUnitsMatch: 50,
|
|
}),
|
|
}
|
|
|
|
testTerms := &terms.AuctioneerTerms{
|
|
OrderExecBaseFee: 1,
|
|
OrderExecFeeRate: 100,
|
|
LeaseDurationBuckets: map[uint32]auctioneerrpc.DurationBucketState{
|
|
2016: auctioneerrpc.DurationBucketState_MARKET_OPEN,
|
|
},
|
|
}
|
|
|
|
testSig := test.NewSignatureFromInt(44, 22)
|
|
|
|
mockSigner.Signature = testSig.Serialize()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
ticket *sidecar.Ticket
|
|
expectedErr string
|
|
}{{
|
|
name: "no ticket",
|
|
expectedErr: "",
|
|
}, {
|
|
name: "empty ticket",
|
|
ticket: &sidecar.Ticket{},
|
|
expectedErr: "invalid sidecar ticket state",
|
|
}, {
|
|
name: "wrong data for state",
|
|
ticket: &sidecar.Ticket{
|
|
State: sidecar.StateRegistered,
|
|
},
|
|
expectedErr: "invalid sidecar ticket, missing recipient",
|
|
}, {
|
|
name: "wrong data for state",
|
|
ticket: &sidecar.Ticket{
|
|
State: sidecar.StateRegistered,
|
|
Recipient: &sidecar.Recipient{
|
|
NodePubKey: acctKeySmall,
|
|
MultiSigPubKey: acctKeySmall,
|
|
},
|
|
},
|
|
expectedErr: "offer in ticket is not signed",
|
|
}, {
|
|
name: "good signature, incorrect offer amount",
|
|
ticket: &sidecar.Ticket{
|
|
State: sidecar.StateRegistered,
|
|
Offer: sidecar.Offer{
|
|
SigOfferDigest: testSig,
|
|
SignPubKey: acctKeySmall,
|
|
},
|
|
Recipient: &sidecar.Recipient{
|
|
NodePubKey: acctKeySmall,
|
|
MultiSigPubKey: acctKeyBig,
|
|
},
|
|
},
|
|
expectedErr: "channel capacity must be positive multiple of",
|
|
}, {
|
|
name: "all valid",
|
|
ticket: &sidecar.Ticket{
|
|
State: sidecar.StateRegistered,
|
|
Offer: sidecar.Offer{
|
|
SigOfferDigest: testSig,
|
|
SignPubKey: acctKeySmall,
|
|
Capacity: 5_000_000,
|
|
},
|
|
Recipient: &sidecar.Recipient{
|
|
NodePubKey: acctKeySmall,
|
|
MultiSigPubKey: acctKeyBig,
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
bid.SidecarTicket = tc.ticket
|
|
if tc.ticket != nil {
|
|
digest, err := tc.ticket.OfferDigest()
|
|
require.NoError(t, err)
|
|
mockSigner.SignatureMsg = string(digest[:])
|
|
}
|
|
|
|
mgr.cfg.Store = newMockStore()
|
|
_, err := mgr.PrepareOrder(
|
|
context.Background(), bid, acct, testTerms,
|
|
)
|
|
|
|
if tc.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
|
|
if tc.ticket != nil {
|
|
require.NotNil(t, tc.ticket.Order)
|
|
require.Equal(
|
|
t, testSig,
|
|
tc.ticket.Order.SigOrderDigest,
|
|
)
|
|
require.Equal(
|
|
t, [32]byte(bid.nonce),
|
|
tc.ticket.Order.BidNonce,
|
|
)
|
|
}
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
}
|
|
}
|
|
}
|