cmd/pool: add sidecar commands for offer/register/expect

With the RPC methods now in place, we add a new sidecar subcommand to
our CLI and add verbs for interacting with sidecars to it.
Because the ticket is in an encoded form we also add a printticket
subcommand for easy inspection.
This commit is contained in:
Oliver Gugger 2021-03-19 11:50:09 +01:00
parent 15eeee2a3e
commit 14da89be07
No known key found for this signature in database
GPG key ID: 8E4256593F177720
3 changed files with 385 additions and 5 deletions

View file

@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@ -17,6 +16,7 @@ import (
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool"
"github.com/lightninglabs/pool/poolrpc"
"github.com/lightninglabs/protobuf-hex-display/json"
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
"github.com/lightninglabs/protobuf-hex-display/proto"
"github.com/lightningnetwork/lnd/lncfg"
@ -133,6 +133,7 @@ func main() {
}
app.Commands = append(app.Commands, accountsCommands...)
app.Commands = append(app.Commands, ordersCommands...)
app.Commands = append(app.Commands, sidecarCommands...)
app.Commands = append(app.Commands, auctionCommands...)
app.Commands = append(app.Commands, listAuthCommand)
app.Commands = append(app.Commands, getInfoCommand)

View file

@ -2,6 +2,7 @@ package main
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fmt"
@ -283,7 +284,7 @@ func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl
ask.LeaseDurationBlocks,
chainfee.SatPerKWeight(
ask.Details.MaxBatchFeeRateSatPerKw,
), true,
), true, nil,
); err != nil {
return fmt.Errorf("unable to print order details: %v", err)
}
@ -313,7 +314,7 @@ func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl
func printOrderDetails(client poolrpc.TraderClient, amt,
minChanAmt, selfChanBalance btcutil.Amount, rate order.FixedRatePremium,
leaseDuration uint32, maxBatchFeeRate chainfee.SatPerKWeight,
isAsk bool) error {
isAsk bool, sidecarTicket *sidecar.Ticket) error {
auctionFee, err := client.AuctionFee(
context.Background(), &poolrpc.AuctionFeeRequest{},
@ -356,6 +357,12 @@ func printOrderDetails(client poolrpc.TraderClient, amt,
fmt.Printf("Self channel balance: %v\n", selfChanBalance)
}
if sidecarTicket != nil {
fmt.Println("Sidecar order: ")
fmt.Printf(" Recipient node: %x\n",
sidecarTicket.Recipient.NodePubKey.SerializeCompressed())
}
return nil
}
@ -413,6 +420,17 @@ var ordersSubmitBidCommand = cli.Command{
"from our account into the channel; can be " +
"used to create up to 50/50 balanced channels",
},
cli.StringFlag{
Name: "sidecar_ticket",
Usage: "instead of leasing a channel for the node " +
"connected to this pool instance, lease a " +
"channel for another node; use the " +
"information within the ticket to identify " +
"the receiver of the sidecar channel; using " +
"a sidecar ticket will also overwrite the " +
"amt, min_chan_amt, lease_duration_blocks " +
"and self_chan_balance fields",
},
}, sharedFlags...),
Action: ordersSubmitBid,
}
@ -444,10 +462,54 @@ func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl
bid := &poolrpc.Bid{
LeaseDurationBlocks: uint32(ctx.Uint64("lease_duration_blocks")),
Version: uint32(order.VersionSelfChanBalance),
Version: uint32(order.VersionSidecarChannel),
MinNodeTier: nodeTier,
}
// Let's find out if this is an order for a sidecar channel because if
// it is, we can take some of the information out of the ticket and
// don't require the user to enter them manually again.
var ticket *sidecar.Ticket
if ctx.IsSet("sidecar_ticket") {
// The ticket is expected in the string encoded version which
// has a prefix and a checksum. We're supposed to send it to
// the daemon in its raw format though. So let's decode and
// check it in the process.
ticket, err = sidecar.DecodeString(ctx.String("sidecar_ticket"))
if err != nil {
return fmt.Errorf("unable to parse sidecar ticket: %v",
err)
}
// Let's make sure the ticket is in the correct state. This will
// be checked by the server as well but we want to make sure we
// don't run into a nil reference when printing the order
// details below.
if ticket.State != sidecar.StateRegistered ||
ticket.Recipient == nil {
return fmt.Errorf("unexpected sidecar ticket state "+
"%d, possibly not registered with recipient "+
"node yet", ticket.State)
}
// With the ticket parsed and formally checked, we can now pre-
// fill the amount and min channel amount. Those values must
// match the offered capacity, otherwise the push amount won't
// work as expected and the protocol would get more complex as
// well.
amtStr := fmt.Sprintf("%d", ticket.Offer.Capacity)
pushAmtStr := fmt.Sprintf("%d", ticket.Offer.PushAmt)
_ = ctx.Set("amt", amtStr)
_ = ctx.Set("min_chan_amt", amtStr)
_ = ctx.Set("self_chan_balance", pushAmtStr)
bid.LeaseDurationBlocks = ticket.Offer.LeaseDurationBlocks
// Looks good so far. The rest will be checked server side. For
// now we can just add the ticket to the order.
bid.SidecarTicket = ctx.String("sidecar_ticket")
}
params, err := parseCommonParams(ctx, bid.LeaseDurationBlocks)
if err != nil {
return fmt.Errorf("unable to parse order params: %v", err)
@ -495,7 +557,7 @@ func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl
bid.LeaseDurationBlocks,
chainfee.SatPerKWeight(
bid.Details.MaxBatchFeeRateSatPerKw,
), false,
), false, ticket,
); err != nil {
return fmt.Errorf("unable to print order details: %v", err)
}
@ -517,8 +579,38 @@ func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl
if err != nil {
return err
}
printRespJSON(resp)
// If there was a sidecar ticket, we now also need to display the
// updated ticket that contains the order nonce (and a signature over
// that). This ticket needs to be given to the recipient for them to
// initiate the last step of the sidecar channel protocol.
// The signed ticket is available in the output of the ListOrders call.
if ticket != nil {
newBidNonce := resp.GetAcceptedOrderNonce()
allOrders, err := client.ListOrders(
context.Background(), &poolrpc.ListOrdersRequest{
ActiveOnly: true,
},
)
if err != nil {
return fmt.Errorf("error listing orders to print "+
"updated sidecar ticket: %v", err)
}
// Find the order we just created.
for _, bid := range allOrders.Bids {
if bytes.Equal(newBidNonce, bid.Details.OrderNonce) {
printJSON(struct {
Ticket string `json:"ticket"`
}{
Ticket: bid.SidecarTicket,
})
}
}
}
return nil
}

287
cmd/pool/sidecar.go Normal file
View file

@ -0,0 +1,287 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/poolrpc"
"github.com/lightninglabs/pool/sidecar"
"github.com/urfave/cli"
)
var sidecarCommands = []cli.Command{
{
Name: "sidecar",
Aliases: []string{"s"},
Usage: "Manage sidecar channels.",
Category: "Orders",
Subcommands: []cli.Command{
sidecarOfferCommand,
sidecarPrintTicketCommand,
sidecarRegisterCommand,
sidecarExpectChannelCommand,
},
},
}
var sidecarOfferCommand = cli.Command{
Name: "offer",
Aliases: []string{"o"},
Usage: "offer a sidecar channel",
ArgsUsage: "capacity self_chan_balance lease_duration_blocks",
Description: `
Creates an offer for providing a sidecar channel to another node.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "capacity",
Usage: "the total channel capacity of the sidecar " +
"channel to offer",
},
cli.Uint64Flag{
Name: "self_chan_balance",
Usage: "the number of satoshis that should be pushed " +
"to the recipient of the sidecar channel as " +
"initial outbound channel balance; amount " +
"will be deducted from account that submits " +
"bid order, reimbursement must happen out of " +
"band, not part of the sidecar protocol",
},
cli.Uint64Flag{
Name: "lease_duration_blocks",
Usage: "the number of blocks the resulting leased " +
"channel should be open for",
Value: uint64(order.LegacyLeaseDurationBucket),
},
},
Action: sidecarOffer,
}
func sidecarOffer(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "offer")
return nil
}
var (
args = ctx.Args()
capacity, pushAmt uint64
duration uint32
)
switch {
case ctx.IsSet("capacity"):
capacity = ctx.Uint64("capacity")
case args.Present():
parsed, err := parseAmt(args.First())
if err != nil {
return fmt.Errorf("unable to decode capacity: %v", err)
}
capacity = uint64(parsed)
args = args.Tail()
}
switch {
case ctx.IsSet("self_chan_balance"):
pushAmt = ctx.Uint64("self_chan_balance")
case args.Present():
parsed, err := parseAmt(args.First())
if err != nil {
return fmt.Errorf("unable to decode self channel "+
"balance: %v", err)
}
pushAmt = uint64(parsed)
args = args.Tail()
}
switch {
case ctx.IsSet("lease_duration_blocks"):
duration = uint32(ctx.Uint64("lease_duration_blocks"))
case args.Present():
duration64, err := strconv.ParseInt(args.First(), 10, 32)
if err != nil {
return fmt.Errorf("unable to parse lease duration "+
"blocks: %v", err)
}
duration = uint32(duration64)
args = args.Tail()
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.OfferSidecar(
context.Background(), &poolrpc.OfferSidecarRequest{
ChannelCapacitySat: capacity,
SelfChanBalance: pushAmt,
LeaseDurationBlocks: duration,
},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
type jsonTicket struct {
ID string
Version uint8
State string
Capacity uint64
PushAmount uint64
LeaseDurationBlocks uint32
OfferSigningPubKey string
RecipientNodePubKey string
RecipientMultiSigPubKey string
RecipientMultiSigKeyIndex uint32
OrderNonce string
}
var sidecarPrintTicketCommand = cli.Command{
Name: "printticket",
Aliases: []string{"p"},
Usage: "decode and print the content of a sidecar ticket",
ArgsUsage: "ticket",
Description: `
Tries to decode the given ticket from the human readable (prefixed)
base64 encoded version.`,
Action: sidecarPrintTicket,
}
func sidecarPrintTicket(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() != 1 || ctx.NumFlags() != 0 {
_ = cli.ShowCommandHelp(ctx, "printticket")
return nil
}
ticketStr := ctx.Args().First()
ticket, err := sidecar.DecodeString(ticketStr)
if err != nil {
return fmt.Errorf("error decoding base64 ticket: %v", err)
}
jsonTicket := &jsonTicket{
ID: hex.EncodeToString(ticket.ID[:]),
Version: uint8(ticket.Version),
State: ticket.State.String(),
Capacity: uint64(ticket.Offer.Capacity),
PushAmount: uint64(ticket.Offer.PushAmt),
LeaseDurationBlocks: ticket.Offer.LeaseDurationBlocks,
}
if ticket.Offer.SignPubKey != nil {
jsonTicket.OfferSigningPubKey = hex.EncodeToString(
ticket.Offer.SignPubKey.SerializeCompressed(),
)
}
if ticket.Recipient != nil {
if ticket.Recipient.NodePubKey != nil {
jsonTicket.RecipientNodePubKey = hex.EncodeToString(
ticket.Recipient.NodePubKey.SerializeCompressed(),
)
}
if ticket.Recipient.MultiSigPubKey != nil {
jsonTicket.RecipientMultiSigPubKey = hex.EncodeToString(
ticket.Recipient.MultiSigPubKey.SerializeCompressed(),
)
}
jsonTicket.RecipientMultiSigKeyIndex = ticket.Recipient.MultiSigKeyIndex
}
if ticket.Order != nil {
jsonTicket.OrderNonce = hex.EncodeToString(
ticket.Order.BidNonce[:],
)
}
printJSON(jsonTicket)
return nil
}
var sidecarRegisterCommand = cli.Command{
Name: "register",
Aliases: []string{"r"},
Usage: "register an incoming sidecar channel and add node info to " +
"ticket",
ArgsUsage: "ticket",
Description: `
Registers a sidecar ticket for an incoming sidecar channel with the node
and adds its recipient information to it, resulting in an updated ticket
that needs to be handed back to the provider.`,
Action: sidecarRegister,
}
func sidecarRegister(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() != 1 || ctx.NumFlags() != 0 {
_ = cli.ShowCommandHelp(ctx, "register")
return nil
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.RegisterSidecar(
context.Background(), &poolrpc.RegisterSidecarRequest{
Ticket: ctx.Args().First(),
},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var sidecarExpectChannelCommand = cli.Command{
Name: "expectchannel",
Aliases: []string{"e"},
Usage: "start waiting for sidecar channel",
ArgsUsage: "ticket",
Description: `
Connect to the auctioneer and wait for a sidecar order to be matched and
a channel being opened to us.`,
Action: sidecarExpectChannel,
}
func sidecarExpectChannel(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "expectchannel")
return nil
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.ExpectSidecarChannel(
context.Background(), &poolrpc.ExpectSidecarChannelRequest{
Ticket: ctx.Args().First(),
})
if err != nil {
return err
}
printRespJSON(resp)
return nil
}