pool/order/interface.go
Oliver Gugger 2f56378898
order+clientdb: add sidecar ticket to bid order
With this commit we add the optional sidecar ticket to bid orders.
Because the node submitting the bid order doesn't necessarily need to be
the node creating the offer, we just store the ticket as is and don't
require it to be in the database already.
2021-04-02 14:18:10 +02:00

763 lines
24 KiB
Go

package order
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/auctioneerrpc"
"github.com/lightninglabs/pool/sidecar"
"github.com/lightninglabs/pool/terms"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
)
// Nonce is a 32 byte pseudo randomly generated unique order ID.
type Nonce [32]byte
// String returns the hex encoded representation of the nonce.
func (n Nonce) String() string {
return hex.EncodeToString(n[:])
}
// Version is the version of an order. We don't use iota for the constants due
// to the order type being persisted to disk.
type Version uint32
const (
// VersionDefault is the default initial version of orders.
VersionDefault Version = 0
// VersionNodeTierMinMatch is the order version that added recognition
// of the new node tier and min matchable order size fields.
VersionNodeTierMinMatch Version = 1
// VersionLeaseDurationBuckets is the order version that added use of
// multiple lease durations. Only orders with this version are allowed
// to use lease durations outside of the default/legacy 2016 block
// duration.
VersionLeaseDurationBuckets Version = 2
// VersionSelfChanBalance is the order version that added use of the
// self channel balance field. Only orders with this version are allowed
// to use the self channel balance field.
VersionSelfChanBalance Version = 3
// VersionSidecarChannel is the order version that added sidecar
// channels for bid orders. Only orders with this version are allowed
// to set the sidecar ticket field on bid orders. Since sidecar orders
// also add the feature of push amounts on the leased channels, this
// affects makers as well. Makers that don't want to support leasing out
// channels with a push amount (because it might screw up their
// accounting or whatever) can opt out by explicitly submitting their
// ask orders with a version previous to this one.
VersionSidecarChannel Version = 4
)
// Type is the type of an order. We don't use iota for the constants due to the
// order type being persisted to disk.
type Type uint8
const (
// TypeAsk is the constant to represent the "ask" order type.
TypeAsk Type = 0
// TypeBid is the constant to represent the "bid" order type.
TypeBid Type = 1
)
// String returns a human read-able string describing the passed order type.
func (t Type) String() string {
switch t {
case TypeAsk:
return "Ask"
case TypeBid:
return "Bid"
default:
return "<unknown>"
}
}
// State describes the different possible states of an order. We don't use iota
// for the constants due to the order state being persisted to disk.
type State uint8
const (
// StateSubmitted is the state an order has after it's been submitted
// successfully.
StateSubmitted State = 0
// StateCleared is the state an order has after it's been accepted as
// part of a batch but has not been executed yet.
StateCleared State = 1
// StatePartiallyFilled is the state an order has after some but not all
// parts of it have been filled.
StatePartiallyFilled State = 2
// StateExecuted is the state an order has after it has been matched
// with another order in the order book and fully processed.
StateExecuted State = 3
// StateCanceled is the state an order has after a user cancels the
// order manually.
StateCanceled State = 4
// StateExpired is the state an order has after it's maximum lifetime
// has passed.
StateExpired State = 5
// StateFailed is the state an order has if any irrecoverable error
// happens in its lifetime.
StateFailed State = 6
)
// String returns a human readable string representation of the order state.
func (s State) String() string {
switch s {
case StateSubmitted:
return "submitted"
case StateCleared:
return "cleared"
case StatePartiallyFilled:
return "partially_filled"
case StateExecuted:
return "executed"
case StateCanceled:
return "canceled"
case StateExpired:
return "expired"
case StateFailed:
return "failed"
default:
return fmt.Sprintf("unknown<%d>", s)
}
}
// Archived returns true if the order is in a state that is considered to be
// fully executed and no more modifications will be done to it.
func (s State) Archived() bool {
switch s {
case StateExecuted, StateCanceled, StateExpired, StateFailed:
return true
default:
return false
}
}
// MatchState describes the distinct phases an order goes through as seen by the
// trader daemon. These states are not persisted on the orders themselves but
// rather as events with timestamps so a user can track what's happening to
// their orders.
type MatchState uint8
const (
// MatchStatePrepare is the state an order is in after the
// OrderMatchPrepare message was received initially.
MatchStatePrepare MatchState = 0
// MatchStatePrepare is the state an order is in after the
// OrderMatchPrepare message was processed successfully and the batch
// was accepted.
MatchStateAccepted MatchState = 1
// MatchStateRejected is the state an order is in after the trader
// rejected it, either as an answer to a OderMatchSignBegin or
// OrderMatchFinalize message from the auctioneer.
MatchStateRejected MatchState = 2
// MatchStateSigned is the state an order is in after the
// OrderMatchSignBegin message was processed successfully.
MatchStateSigned MatchState = 3
// MatchStateFinalized is the state an order is in after the
// OrderMatchFinalize message was processed successfully.
MatchStateFinalized MatchState = 4
)
// String returns a human readable string representation of the match state.
func (s MatchState) String() string {
switch s {
case MatchStatePrepare:
return "prepare"
case MatchStateAccepted:
return "accepted"
case MatchStateSigned:
return "signed"
case MatchStateFinalized:
return "finalized"
case MatchStateRejected:
return "rejected"
default:
return fmt.Sprintf("unknown<%d>", s)
}
}
var (
// ErrInsufficientBalance is the error that is returned if an account
// has insufficient balance to perform a requested action.
ErrInsufficientBalance = errors.New("insufficient account balance")
// ZeroNonce is used to find out if a user-provided nonce is empty.
ZeroNonce Nonce
)
// Order is an interface to allow generic handling of both ask and bid orders
// by both store and manager.
type Order interface {
// Nonce is the unique identifier of each order and MUST be created by
// hashing a new random preimage for each new order. The nonce is what
// is signed in the order signature.
Nonce() Nonce
// Details returns the Kit of the order.
Details() *Kit
// Type returns the order type.
Type() Type
// Digest returns a deterministic SHA256 hash over the contents of an
// order. Deterministic in this context means that if two orders have
// the same content, their digest have to be identical as well.
Digest() ([sha256.Size]byte, error)
// ReservedValue returns the maximum value that could be deducted from
// the account if the order is is matched, and therefore has to be
// reserved to ensure the trader can afford it.
ReservedValue(feeSchedule terms.FeeSchedule) btcutil.Amount
}
// Kit stores all the common fields that are used to express the decision to
// participate in the auction process. A kit is always wrapped by either a bid
// or an ask.
type Kit struct {
// nonce is the hash of the preimage and acts as the unique identifier
// of an order.
nonce Nonce
// Preimage is the randomly generated preimage to the nonce hash. It is
// only known to the trader client.
Preimage lntypes.Preimage
// Version is the feature version of this order. Can be used to
// distinguish between certain feature sets or to signal feature flags.
Version Version
// State is the current state the order is in as it was last seen by the
// client. The real state is tracked on the auction server, so this can
// be out of sync if there was no connection for a while.
State State
// FixedRate is the fixed order rate expressed in parts per million.
FixedRate uint32
// Amt is the order amount in satoshis.
Amt btcutil.Amount
// Units the total amount of units that the target amount maps to.
Units SupplyUnit
// UnitsUnfulfilled is the number of units that have not been filled yet
// and are still available for matching against other orders.
UnitsUnfulfilled SupplyUnit
// MultiSigKeyLocator is the key locator used to obtain the multi sig
// key. This will be needed for operations that require a signature
// under said key and will therefore only be known to the trader client.
// This key will only be derived from the connected lnd after the order
// has been formally validated.
MultiSigKeyLocator keychain.KeyLocator
// MaxBatchFeeRate is is the maximum fee rate the trader is willing to
// pay for the batch transaction, in sat/kW.
MaxBatchFeeRate chainfee.SatPerKWeight
// AcctKey is key of the account the order belongs to.
AcctKey [33]byte
// LeaseDuration identifies how long this order wishes to acquire or
// lease out capital in the Lightning Network for.
LeaseDuration uint32
// MinUnitsMatch signals the minimum number of units that must be
// matched against an order.
MinUnitsMatch SupplyUnit
}
// Nonce is the unique identifier of each order and MUST be created by hashing a
// new random preimage for each new order. The nonce is what is signed in the
// order signature.
//
// NOTE: This method is part of the Order interface.
func (k *Kit) Nonce() Nonce {
return k.nonce
}
// Details returns the Kit of the order.
//
// NOTE: This method is part of the Order interface.
func (k *Kit) Details() *Kit {
return k
}
// NewKitWithPreimage creates a new kit by hashing the preimage to generate the
// unique nonce.
func NewKitWithPreimage(preimage lntypes.Preimage) *Kit {
var nonce Nonce
hash := preimage.Hash()
copy(nonce[:], hash[:])
return &Kit{
nonce: nonce,
Preimage: preimage,
Version: VersionLeaseDurationBuckets,
}
}
// NewKit creates a new kit from a nonce in case the preimage is not known.
func NewKit(nonce Nonce) *Kit {
return &Kit{
nonce: nonce,
Version: VersionLeaseDurationBuckets,
}
}
// Ask is the specific order type representing the willingness of an auction
// participant to lend out their funds by opening channels to other auction
// participants.
type Ask struct {
// Kit contains all the common order parameters.
Kit
}
// Type returns the order type.
//
// NOTE: This method is part of the Order interface.
func (a *Ask) Type() Type {
return TypeAsk
}
// Digest returns a deterministic SHA256 hash over the contents of an ask order.
// Deterministic in this context means that if two orders have the same content,
// their digest have to be identical as well.
//
// NOTE: This method is part of the Order interface.
func (a *Ask) Digest() ([sha256.Size]byte, error) {
var (
msg bytes.Buffer
result [sha256.Size]byte
)
switch a.Kit.Version {
case VersionDefault:
err := lnwire.WriteElements(
&msg, a.nonce[:], uint32(a.Version), a.FixedRate,
a.Amt, a.LeaseDuration, uint64(a.MaxBatchFeeRate),
)
if err != nil {
return result, err
}
case VersionNodeTierMinMatch, VersionLeaseDurationBuckets,
VersionSelfChanBalance, VersionSidecarChannel:
err := lnwire.WriteElements(
&msg, a.nonce[:], uint32(a.Version), a.FixedRate,
a.Amt, a.LeaseDuration, uint64(a.MaxBatchFeeRate),
uint32(a.MinUnitsMatch),
)
if err != nil {
return result, err
}
default:
return result, fmt.Errorf("unknown version %d", a.Kit.Version)
}
return sha256.Sum256(msg.Bytes()), nil
}
// reservedValue returns the maximum value that could be deducted from a single
// account if the given order is matched under the worst case fee conditions.
// This usually means the order is partially matched with the minimum match
// size, all in different batches, leading to maximum chain and execution fees
// being paid.
//
// The passed function should be set to either calculate the maker or taker
// balance delta for a single match of the given amount.
func reservedValue(o Order,
perMatchDelta func(btcutil.Amount) btcutil.Amount) btcutil.Amount {
// If this order is in a state where it cannot be matched, return 0.
if o.Details().State.Archived() {
return 0
}
// The situation where the trader needs to pay the largest amount of
// fees is when the order gets partially matched by its minimum possible
// units per batch. This situation results in the most chain and
// execution fees possible.
totalSats := o.Details().UnitsUnfulfilled.ToSatoshis()
minMatchSize := o.Details().MinUnitsMatch.ToSatoshis()
maxNumMatches := totalSats / minMatchSize
// We handle the case where the last match consumes the remainder of
// the order size.
rem := btcutil.Amount(0)
if maxNumMatches*minMatchSize < totalSats {
maxNumMatches--
rem = totalSats - maxNumMatches*minMatchSize
}
// We'll calculate the worst case possible wrt. fees paid by the
// account if the order is filled by minimum size matched.
balanceDelta := maxNumMatches * perMatchDelta(minMatchSize)
if rem > 0 {
balanceDelta += perMatchDelta(rem)
}
// Subtract the worst case chain fee from the balance.
maxFeeRate := o.Details().MaxBatchFeeRate
balanceDelta -= maxNumMatches * EstimateTraderFee(1, maxFeeRate)
if rem > 0 {
balanceDelta -= EstimateTraderFee(1, maxFeeRate)
}
// If the balance delta is negative, meaning this order will decrease
// the balance, the reserved value is the negative balance delta.
if balanceDelta < 0 {
return -balanceDelta
}
// Otherwise this order will increase the balance if matched, and we
// don't have to reserve any amount.
return 0
}
// ReservedValue returns the maximum value that could be deducted from a single
// account if the ask is is matched under the worst case fee conditions.
func (a *Ask) ReservedValue(feeSchedule terms.FeeSchedule) btcutil.Amount {
// For an ask the clearing price will be no lower than the ask's fixed
// rate, resulting in the smallest gain for the asker.
clearingPrice := FixedRatePremium(a.FixedRate)
return reservedValue(a, func(amt btcutil.Amount) btcutil.Amount {
delta, _, _ := makerDelta(
feeSchedule, clearingPrice, amt, a.LeaseDuration,
)
return delta
})
}
// NodeTier an enum-like variable that presents which "tier" a node is in. A
// higher tier is better. Node tiers are used to allow clients to express their
// preference w.r.t the "quality" of a node they wish to buy channels from.
type NodeTier uint32
const (
// NodeTierDefault only exists in-memory as allows users to specify
// that they want to opt-into the default "node tier". The
// DefaultMinNodeTier constant should point to what the current default
// node tier is.
NodeTierDefault NodeTier = 0
// NodeTier0 is the tier for nodes which may not be explicitly ranked.
// Orders submitted with this min tier express that they don't care
// about the "quality" of the node they're matched with.
NodeTier0 NodeTier = 1
// NodeTier1 is the "base" node tier. Nodes on this tier are considered
// to be relatively good. We have this be the first value in the enum
// so it can be the default within the codebase and for order
// submission/matching.
NodeTier1 NodeTier = 2
)
// DefaultMinNodeTier is the default node tier. With this current value, Bids
// will default to only matching with nodes in the first tier and above.
const DefaultMinNodeTier = NodeTier1
// String returns the string representation of the target NodeTier.
func (n NodeTier) String() string {
switch n {
case NodeTier0:
return "NodeTier0"
case NodeTier1:
return "NodeTier1"
case NodeTierDefault:
return "NodeTierDefault"
default:
return fmt.Sprintf("UnknownNodeTier(%v)", uint32(n))
}
}
// Bid is the specific order type representing the willingness of an auction
// participant to pay for inbound liquidity provided by other auction
// participants.
type Bid struct {
// Kit contains all the common order parameters.
Kit
// MinNodeTier is the minimum node tier that this order should be
// matched with. Only Asks backed by nodes on this tier or above will
// be matched with this bid.
MinNodeTier NodeTier
// SelfChanBalance is the initial outbound balance that should be added
// to the channel resulting from matching this bid by moving additional
// funds from the taker's account into the channel.
SelfChanBalance btcutil.Amount
// SidecarTicket indicates, if non-nil, that the channel being purchased
// with this bid should be opened to a node other than the caller's
// node. The lease recipient is another Pool (light) node that
// authenticates itself to the auctioneer using the information in this
// ticket (the information exchange between bidder and lease recipient
// happens out of band). This will only be used if the order version is
// VersionSidecarChannel or greater.
SidecarTicket *sidecar.Ticket
}
// Type returns the order type.
//
// NOTE: This method is part of the Order interface.
func (b *Bid) Type() Type {
return TypeBid
}
// Digest returns a deterministic SHA256 hash over the contents of a bid order.
// Deterministic in this context means that if two orders have the same content,
// their digest have to be identical as well.
//
// NOTE: This method is part of the Order interface.
func (b *Bid) Digest() ([sha256.Size]byte, error) {
var (
msg bytes.Buffer
result [sha256.Size]byte
)
switch b.Kit.Version {
case VersionDefault:
err := lnwire.WriteElements(
&msg, b.nonce[:], uint32(b.Version), b.FixedRate,
b.Amt, b.LeaseDuration, uint64(b.MaxBatchFeeRate),
)
if err != nil {
return result, err
}
case VersionNodeTierMinMatch, VersionLeaseDurationBuckets:
err := lnwire.WriteElements(
&msg, b.nonce[:], uint32(b.Version), b.FixedRate,
b.Amt, b.LeaseDuration, uint64(b.MaxBatchFeeRate),
uint32(b.MinNodeTier), uint32(b.MinUnitsMatch),
)
if err != nil {
return result, err
}
case VersionSelfChanBalance:
err := lnwire.WriteElements(
&msg, b.nonce[:], uint32(b.Version), b.FixedRate,
b.Amt, b.LeaseDuration, uint64(b.MaxBatchFeeRate),
uint32(b.MinNodeTier), uint32(b.MinUnitsMatch),
uint64(b.SelfChanBalance),
)
if err != nil {
return result, err
}
case VersionSidecarChannel:
var isSidecar uint8
if b.SidecarTicket != nil {
isSidecar = 1
}
err := lnwire.WriteElements(
&msg, b.nonce[:], uint32(b.Version), b.FixedRate,
b.Amt, b.LeaseDuration, uint64(b.MaxBatchFeeRate),
uint32(b.MinNodeTier), uint32(b.MinUnitsMatch),
uint64(b.SelfChanBalance), isSidecar,
)
if err != nil {
return result, err
}
default:
return result, fmt.Errorf("unknown version %d", b.Kit.Version)
}
return sha256.Sum256(msg.Bytes()), nil
}
// ReservedValue returns the maximum value that could be deducted from a single
// account if the bid is is matched under the worst case fee conditions.
func (b *Bid) ReservedValue(feeSchedule terms.FeeSchedule) btcutil.Amount {
// For a bid, the final clearing price is never higher that the bid's
// fixed rate, resulting in the highest possible premium paid by the
// bidder.
clearingPrice := FixedRatePremium(b.FixedRate)
return reservedValue(b, func(amt btcutil.Amount) btcutil.Amount {
delta, _, _ := takerDelta(
feeSchedule, clearingPrice, amt, b.SelfChanBalance,
b.LeaseDuration,
)
return delta
})
}
// ValidateSelfChanBalance makes sure that all conditions to use the
// SelfChanBalance field on a bid order are met.
func (b *Bid) ValidateSelfChanBalance() error {
if b.Version < VersionSelfChanBalance {
return fmt.Errorf("cannot use self chan balance with old " +
"order version")
}
if err := sidecar.CheckOfferParams(
b.Amt, b.SelfChanBalance, BaseSupplyUnit,
); err != nil {
return fmt.Errorf("invalid self chan balance: %v", err)
}
if b.Units != b.MinUnitsMatch {
return fmt.Errorf("to use self chan balance the min units " +
"match must be equal to the order amount in units")
}
return nil
}
// This is a compile time check to make certain that both Ask and Bid implement
// the Order interface.
var _ Order = (*Ask)(nil)
var _ Order = (*Bid)(nil)
// Modifier abstracts the modification of an account through a function.
type Modifier func(*Kit)
// StateModifier is a functional option that modifies the state of an order.
func StateModifier(state State) Modifier {
return func(order *Kit) {
order.State = state
}
}
// UnitsFulfilledModifier is a functional option that modifies the number of
// unfulfilled units of an order.
func UnitsFulfilledModifier(newUnfulfilledUnits SupplyUnit) Modifier {
return func(order *Kit) {
order.UnitsUnfulfilled = newUnfulfilledUnits
}
}
// Store is the interface a store has to implement to support persisting orders.
type Store interface {
// SubmitOrder stores an order by using the orders's nonce as an
// identifier. If an order with the given nonce already exists in the
// store, ErrOrderExists is returned.
SubmitOrder(Order) error
// UpdateOrder updates an order in the database according to the given
// modifiers.
UpdateOrder(Nonce, ...Modifier) error
// UpdateOrders atomically updates a list of orders in the database
// according to the given modifiers.
UpdateOrders([]Nonce, [][]Modifier) error
// GetOrder returns an order by looking up the nonce. If no order with
// that nonce exists in the store, ErrNoOrder is returned.
GetOrder(Nonce) (Order, error)
// GetOrders returns all orders that are currently known to the store.
GetOrders() ([]Order, error)
// StorePendingBatch atomically stages all modified orders/accounts as a
// result of a pending batch. If any single operation fails, the whole
// set of changes is rolled back. Once the batch has been
// finalized/confirmed on-chain, then the stage modifications will be
// applied atomically as a result of MarkBatchComplete.
StorePendingBatch(_ *Batch, orders []Nonce,
orderModifiers [][]Modifier, accounts []*account.Account,
accountModifiers [][]account.Modifier) error
// MarkBatchComplete marks a pending batch as complete, applying any
// staged modifications necessary, and allowing a trader to participate
// in a new batch. If a pending batch is not found, ErrNoPendingBatch is
// returned.
MarkBatchComplete() error
}
// UserError is an error type that is returned if an action fails because of
// an invalid action or information provided by the user.
type UserError struct {
FailMsg string
Details *auctioneerrpc.InvalidOrder
}
// Error returns the string representation of the underlying failure message.
func (e *UserError) Error() string {
return e.FailMsg
}
// A compile-time constraint to ensure UserError implements the error interface.
var _ error = (*UserError)(nil)
// ServerOrderParams is the list of values that we have to send to the server
// when submitting an order that doesn't need to be persisted in the local DB.
type ServerOrderParams struct {
// MultiSigKey is a key of the node creating the order that will be used
// to craft the channel funding TX's 2-of-2 multi signature output.
MultiSigKey [33]byte
// NodePubkey is the identity public key of the node submitting the
// order.
NodePubkey [33]byte
// Addrs is a list of network addresses through which the node
// submitting the order can be reached.
Addrs []net.Addr
// RawSig is the raw signature over the order digest signed with the
// trader's account key.
RawSig []byte
}
// PendingChanKey calculates the pending channel ID to be used for funding
// purposes for a given bid and ask. The pending channel ID must be unique, so
// we use the hash of the concatenation of the two nonces: sha256(askNonce ||
// bidNonce).
func PendingChanKey(askNonce, bidNonce Nonce) [32]byte {
var pid [32]byte
h := sha256.New()
_, _ = h.Write(askNonce[:])
_, _ = h.Write(bidNonce[:])
copy(pid[:], h.Sum(nil))
return pid
}