Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion auctioneer/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,21 @@ func (c *Client) SubmitOrder(ctx context.Context, o order.Order,
return fmt.Errorf("unhandled channel type %v", c)
}

var auctionType auctioneerrpc.AuctionType
switch o.Details().AuctionType {
case order.BTCInboundLiquidity:
auctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_INBOUND_LIQUIDITY
case order.BTCOutboundLiquidity:
auctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY
}
minChanAmt := uint64(o.Details().MinUnitsMatch.ToSatoshis())

details := &auctioneerrpc.ServerOrder{
TraderKey: o.Details().AcctKey[:],
AuctionType: auctionType,
RateFixed: o.Details().FixedRate,
Amt: uint64(o.Details().Amt),
MinChanAmt: uint64(o.Details().MinUnitsMatch.ToSatoshis()),
MinChanAmt: minChanAmt,
OrderNonce: nonce[:],
OrderSig: serverParams.RawSig,
MultiSigKey: serverParams.MultiSigKey[:],
Expand Down
1,556 changes: 814 additions & 742 deletions auctioneerrpc/auctioneer.pb.go

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions auctioneerrpc/auctioneer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,20 @@ enum OrderChannelType {
ORDER_CHANNEL_TYPE_SCRIPT_ENFORCED = 2;
}

enum AuctionType {
/*
Default auction type where the bidder is paying for getting bitcoin inbound
liqiudity from the asker.
*/
AUCTION_TYPE_BTC_INBOUND_LIQUIDITY = 0;

/*
Auction type where the bidder is paying the asker to accept a channel
(bitcoin outbound liquidity) from the bidder.
*/
AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY = 1;
}

message ServerOrder {
/*
The trader's account key of the account to use for the order.
Expand Down Expand Up @@ -975,6 +989,9 @@ message ServerOrder {
// with the `allowed_node_ids` field.
repeated bytes not_allowed_node_ids = 15;

// Auction type where this order must be considered during the matching.
AuctionType auction_type = 16;

// Flag used to signal that this order can be shared in public market
// places.
bool is_public = 17;
Expand Down Expand Up @@ -1043,9 +1060,9 @@ message ServerBid {
Give the incoming channel that results from this bid being matched an
initial outbound balance by adding additional funds from the taker's account
into the channel. As a simplification for the execution protocol and the
channel reserve calculations, the self_chan_balance can be at most the same
as the order amount and the min_chan_amt must be set to the full order
amount.
channel reserve calculations the min_chan_amt must be set to the full order
amount. For the inbound liquidity market the self_chan_balance can be at
most the same as the order amount.
*/
uint64 self_chan_balance = 6;

Expand Down
15 changes: 15 additions & 0 deletions clientdb/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const (
// to store the channel confirmation match preferences.
askChannelConfirmationConstraintsType tlv.Type = 9

// orderAuctionType is the tlv type that we use to store the auction
// type for an order.
orderAuctionType tlv.Type = 10

// orderIsPublicType is the tlv type we use to store a flag value when
// it is ok to share details of this order in public markets.
orderIsPublicType tlv.Type = 11
Expand Down Expand Up @@ -698,6 +702,7 @@ func deserializeOrderTlvData(r io.Reader, o order.Order) error {
askAnnouncementConstraints uint8
bidZeroConf uint8
askConfirmationConstraints uint8
auctionType uint8
isPublic uint8
)

Expand Down Expand Up @@ -726,6 +731,7 @@ func deserializeOrderTlvData(r io.Reader, o order.Order) error {
askChannelConfirmationConstraintsType,
&askConfirmationConstraints,
),
tlv.MakePrimitiveRecord(orderAuctionType, &auctionType),
tlv.MakePrimitiveRecord(orderIsPublicType, &isPublic),
)
if err != nil {
Expand Down Expand Up @@ -804,6 +810,10 @@ func deserializeOrderTlvData(r io.Reader, o order.Order) error {
o.Details().NotAllowedNodeIDs = nodeIDs
}

if t, ok := parsedTypes[orderAuctionType]; ok && t == nil {
o.Details().AuctionType = order.AuctionType(auctionType)
}

t, ok := parsedTypes[orderIsPublicType]
if ok && t == nil && isPublic == 1 {
o.Details().IsPublic = true
Expand Down Expand Up @@ -905,6 +915,11 @@ func serializeOrderTlvData(w io.Writer, o order.Order) error {
&askConfirmationConstraints,
))

auctionType := uint8(o.Details().AuctionType)
tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord(
orderAuctionType, &auctionType,
))

if o.Details().IsPublic {
isPublic := uint8(1)
tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord(
Expand Down
2 changes: 2 additions & 0 deletions clientdb/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var submitOrderTestCases = []struct {
}
o.Details().MinUnitsMatch = 10
o.Details().ChannelType = order.ChannelTypeScriptEnforced
o.Details().AuctionType = order.BTCOutboundLiquidity

// It is not possible for an order to have AllowedNodeIDs and
// NotAllowedNodeIDs at the same time but we want to test
Expand Down Expand Up @@ -192,6 +193,7 @@ func dummyOrder(amt btcutil.Amount, leaseDuration uint32) *order.Kit {
panic(fmt.Sprintf("could not create private key: %v", err))
}
kit := order.NewKitWithPreimage(testPreimage)
kit.AuctionType = order.BTCInboundLiquidity
kit.Version = order.VersionLeaseDurationBuckets
kit.State = order.StateExecuted
kit.FixedRate = 21
Expand Down
101 changes: 96 additions & 5 deletions cmd/pool/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,34 @@ const (
channelTypePeerDependent = "legacy"
channelTypeScriptEnforced = "script-enforced"

auctionTypeInboundLiquidity = "inbound"
auctionTypeOutboundLiquidity = "outbound"

defaultMaxBatchFeeRateSatPerVByte = 100

defaultConfirmationConstraints = 1

defaultAuctionType = auctionTypeInboundLiquidity

submitOrderDescription = `
Submit a new order in the given market. Currently, Pool supports two
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great description 👍

types of orders: asks, and bids. In Pool, market participants buy/sell
liquidity in _units_. A unit is 100,000 satoshis.

In the *inbound* market, the bidder pays the asker a premium for a
channel with 50% to 100% of inbound liquidity. The premium amount is
calculated using the funding amount of the asker.

In the outbound market, the bidder pays the asker a premium for
a channel with outbound liquidity.
To participate in the outbound market the asker needs to create an order
with '--amount=100000' (one unit) and the '--min_chan_amt' equals to
the minimum channel size that is willing to accept.
The bidder needs to create an order with '--amount=100000' (one unit)
and '--self_chan_balance' equals to the funds that is willing to commit
in a channel. The premium amount is calculated using the funding amount
of the bidder ('--self_chan_balance').
`
)

var ordersCommands = []cli.Command{
Expand All @@ -40,9 +65,10 @@ var ordersCommands = []cli.Command{
ordersListCommand,
ordersCancelCommand,
{
Name: "submit",
Aliases: []string{"s"},
Usage: "submit an order",
Name: "submit",
Aliases: []string{"s"},
Usage: "submit an order",
Description: submitOrderDescription,
Subcommands: []cli.Command{
ordersSubmitAskCommand,
ordersSubmitBidCommand,
Expand Down Expand Up @@ -129,6 +155,14 @@ var sharedFlags = []cli.Flag{
channelTypePeerDependent,
channelTypeScriptEnforced),
},
cli.StringFlag{
Name: "auction_type",
Usage: fmt.Sprintf("the auction market where this offer "+
"must be considered in during matching (%q, %q)",
auctionTypeInboundLiquidity,
auctionTypeOutboundLiquidity),
Value: defaultAuctionType,
},
cli.StringSliceFlag{
Name: "allowed_node_id",
Usage: "the list of nodes this order is allowed to match " +
Expand Down Expand Up @@ -287,12 +321,26 @@ func parseCommonParams(ctx *cli.Context, blockDuration uint32) (*poolrpc.Order,
break
case channelTypePeerDependent:
params.ChannelType = auctioneerrpc.OrderChannelType_ORDER_CHANNEL_TYPE_PEER_DEPENDENT

case channelTypeScriptEnforced:
params.ChannelType = auctioneerrpc.OrderChannelType_ORDER_CHANNEL_TYPE_SCRIPT_ENFORCED

default:
return nil, fmt.Errorf("unknown channel type %q", channelType)
}

auctionType := ctx.String("auction_type")
switch auctionType {
case auctionTypeInboundLiquidity:
params.AuctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_INBOUND_LIQUIDITY

case auctionTypeOutboundLiquidity:
params.AuctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY

default:
return nil, fmt.Errorf("unknown auction type %q", channelType)
}

// Get the list of node ids this order is allowed/not allowed to match
// with.
allowedNodeIDs, err := parseNodePubKeySlice(ctx, "allowed_node_id")
Expand Down Expand Up @@ -457,6 +505,28 @@ func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl

ask.Details = params

// Checks for the outbound liquidity market.
if ask.Details.AuctionType == auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY {
var err error
baseSupply := uint64(order.BaseSupplyUnit)
switch {
case ask.Details.Amt != baseSupply:
err = fmt.Errorf("The order amount be exactly the "+
"base supply amount (%v sats)", baseSupply)

case !ctx.IsSet("min_chan_amt") ||
ctx.Uint64("min_chan_amt") == 0:

err = fmt.Errorf("The `min_chan_amt` parameter must "+
"be set to at least %v sats", baseSupply)
}

if err != nil {
return fmt.Errorf("unable to participate in the "+
"outbound liquidity market. %v", err)
}
}

client, cleanup, err := getClient(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -672,8 +742,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 {
Expand All @@ -688,6 +759,26 @@ func parseBaseBid(ctx *cli.Context) (*poolrpc.Bid, *sidecar.Ticket, error) {
}
}

// Checks for the outbound liquidity market.
if bid.Details.AuctionType == auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY {
var err error
baseSupply := uint64(order.BaseSupplyUnit)
switch {
case bid.Details.Amt != baseSupply:
err = fmt.Errorf("The order amount must be exactly "+
"the base supply amount (%v sats)", baseSupply)

case bid.SelfChanBalance < baseSupply:
err = fmt.Errorf("The `self_chan_balance` parameter "+
"must be set to at least %v sats", baseSupply)
}
if err != nil {
return nil, nil, fmt.Errorf("The unable to"+
"participate in the outbound liquidity "+
"market. %v", err)
}
}

return bid, ticket, nil
}

Expand Down
11 changes: 11 additions & 0 deletions docs/orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,17 @@ To cancel an order. First get the order_nonce of the to be canceled order by usi
$ pool orders cancel order_nonce
```

## Auction types

Currently there are two kind of auction types supported in pool:

* **BTC inbound liquidity market**: the bidder pays the asker for getting a channel where most of the funding liquidity comes from the asker. The bidder pays a premium based on the asker liquidity supply.

* **BTC outbound liquidity market**: the bidder pays the asker for opening a channel where most of the funding liquidity comes from the bidder side. The bidder pays a premium based on the bidder liquidity supply.
In this kind of auctions the asker needs to set the order amount to 100k and specify the `min_chan_amt` that will be matched against the bidder's `self_chan_balance`. The bidder needs to set the order amount to 100k and a `self_chan_balance` greater or equal than 100k.
Sidecar tickets are not supported in this kind of auction yet.


## Extensibility

At the moment there are only two restrictions that users can set on _who_ their orders will be matched against. There is the global `--newnodesonly` flag which will cause the trader daemon to reject any matches with nodes that its connected `lnd` node already has channels with.
Expand Down
9 changes: 8 additions & 1 deletion funding/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
44 changes: 41 additions & 3 deletions funding/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Loading