Skip to content

Commit

Permalink
Merge pull request #287 from lightninglabs/sidecar-crud
Browse files Browse the repository at this point in the history
sidecar: add ListSidecars and CancelSidecar RPCs
  • Loading branch information
Roasbeef authored Sep 30, 2021
2 parents f633d70 + 3f8f556 commit 53df9a3
Show file tree
Hide file tree
Showing 14 changed files with 1,534 additions and 686 deletions.
18 changes: 10 additions & 8 deletions auto_sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ func deriveStreamID(ticket *sidecar.Ticket, provider bool) ([64]byte, error) {
// SidecarDriver houses a series of methods needed to drive a given sidecar
// channel towards completion.
type SidecarDriver interface {
// ValidateOrderedTicketctx attempts to validate that a given ticket
// has rpoerly transitioned to the ordered state.
// ValidateOrderedTicket attempts to validate that a given ticket
// has properly transitioned to the ordered state.
ValidateOrderedTicket(tkt *sidecar.Ticket) error

// ExpectChannel is called by the receiver of a channel once the
Expand All @@ -130,8 +130,8 @@ type SidecarDriver interface {
// storage.
UpdateSidecar(tkt *sidecar.Ticket) error

// SubmitOrder submits a bid derived from the sidecar ticket, account,
// and bid template to the auctioneer.
// SubmitSidecarOrder submits a bid derived from the sidecar ticket,
// account, and bid template to the auctioneer.
SubmitSidecarOrder(*sidecar.Ticket, *order.Bid,
*account.Account) (*sidecar.Ticket, error)
}
Expand Down Expand Up @@ -207,7 +207,8 @@ func (a *SidecarNegotiator) Start() error {
if a.cfg.Provider {
a.wg.Add(1)
go a.autoSidecarProvider(
ctx, a.cfg.StartingPkt, a.cfg.ProviderBid, a.cfg.ProviderAccount,
ctx, a.cfg.StartingPkt, a.cfg.ProviderBid,
a.cfg.ProviderAccount,
)
} else {
a.wg.Add(1)
Expand Down Expand Up @@ -474,8 +475,8 @@ func (a *SidecarNegotiator) stateStepRecipient(ctx context.Context,
// autoSidecarProvider is a goroutine that will attempt to advance a new
// sidecar ticket through the negotiation process until it reaches its final
// state.
func (a *SidecarNegotiator) autoSidecarProvider(ctx context.Context, startingPkt *SidecarPacket,
bid *order.Bid, acct *account.Account) {
func (a *SidecarNegotiator) autoSidecarProvider(ctx context.Context,
startingPkt *SidecarPacket, bid *order.Bid, acct *account.Account) {

defer a.wg.Done()

Expand Down Expand Up @@ -619,7 +620,8 @@ func (a *SidecarNegotiator) CurrentState() sidecar.State {
// sidecar ticket. It takes the current transcript state, the provider's
// account, and canned bid and returns a new transition to a new ticket state.
func (a *SidecarNegotiator) stateStepProvider(ctx context.Context,
pkt *SidecarPacket, bid *order.Bid, acct *account.Account) (*SidecarPacket, error) {
pkt *SidecarPacket, bid *order.Bid,
acct *account.Account) (*SidecarPacket, error) {

switch {
// In this case, we've just restarted, so we'll attempt to start from
Expand Down
86 changes: 84 additions & 2 deletions clientdb/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,16 @@ func (db *DB) UpdateSidecar(ticket *sidecar.Ticket) error {
return ErrNoSidecar
}

// TODO(roasbeef): remove the bid if in the final state now/
// If the ticket is in a terminal state, we won't ever need the
// bid template again, so we remove it (if it still exists).
if ticket.State.IsTerminal() && ticket.Order != nil {
err := removeBidTemplate(
sidecarBucket, ticket.Order.BidNonce,
)
if err != nil {
return err
}
}

return storeSidecar(sidecarBucket, sidecarKey, ticket)
})
Expand Down Expand Up @@ -170,6 +179,51 @@ func (db *DB) Sidecar(id [8]byte,
return s, nil
}

// SidecarsByID returns all sidecar tickets with the given ID. Normally this is
// just a single ticket. But because the ID is just 8 bytes and is randomly
// generated, there could be collisions, especially since tickets can also be
// crafted by a malicious party and given to any node. That's why the offer's
// public key is also used as an identifying element since that cannot easily be
// forged without also producing a valid signature. So an attacker cannot
// overwrite a ticket a node offered by themselves offering a ticket with the
// same ID and tricking the victim into registering that.
func (db *DB) SidecarsByID(id [8]byte) ([]*sidecar.Ticket, error) {
var res []*sidecar.Ticket
err := db.View(func(tx *bbolt.Tx) error {
sidecarBucket, err := getBucket(tx, sidecarsBucketKey)
if err != nil {
return err
}

// The first 8 bytes of the key is a sidecar ticket's ID. So as
// long as the key's prefix is matching, we still have tickets
// with the same ID.
cursor := sidecarBucket.Cursor()
key, val := cursor.Seek(id[:])
for ; key != nil && bytes.HasPrefix(key, id[:]); key, val = cursor.Next() {
// The main sidecar bucket has a sub-bucket that's used
// to store order bid information, so we'll skip this
// bucket when attempting to read out all the tickets.
if val == nil {
return nil
}

s, err := readSidecar(sidecarBucket, key)
if err != nil {
return err
}
res = append(res, s)
}

return nil
})
if err != nil {
return nil, err
}

return res, nil
}

// SidecarBidTemplate attempts to retrieve a bid template associated with the
// passed sidecar ticket.
func (db *DB) SidecarBidTemplate(ticket *sidecar.Ticket) (*order.Bid, error) {
Expand Down Expand Up @@ -253,7 +307,9 @@ func readSidecar(sourceBucket *bbolt.Bucket, id []byte) (*sidecar.Ticket,
return sidecar.DeserializeTicket(bytes.NewReader(sidecarBytes))
}

func storeBidTemplate(bidBucket *bbolt.Bucket, bid *order.Bid, ticketNonce order.Nonce) error {
func storeBidTemplate(bidBucket *bbolt.Bucket, bid *order.Bid,
ticketNonce order.Nonce) error {

var w bytes.Buffer
if err := SerializeOrder(bid, &w); err != nil {
return err
Expand Down Expand Up @@ -313,3 +369,29 @@ func readBidTemplate(bidBucket *bbolt.Bucket,

return o.(*order.Bid), nil
}

func removeBidTemplate(sidecarBucket *bbolt.Bucket,
ticketNonce order.Nonce) error {

bidBucket := sidecarBucket.Bucket(bidTemplateBucket)

// If there is no bucket for the bid templates yet, we don't have
// anything to do.
if bidBucket == nil {
return nil
}

// If the ticket nonce wasn't set, there won't be a template stored for
// it either.
if ticketNonce == order.ZeroNonce {
return nil
}

// If the template was already removed, we ignore the error.
err := bidBucket.DeleteBucket(ticketNonce[:])
if err != bbolt.ErrBucketNotFound {
return err
}

return nil
}
75 changes: 75 additions & 0 deletions clientdb/sidecar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,79 @@ func TestSidecarsWithOrder(t *testing.T) {

// This bid should match the one we inserted earlier exactly.
require.Equal(t, diskBid, bid)

// Once we put the ticket into a final state, the bid template for it
// should be deleted.
ticket.State = sidecar.StateCanceled
err = db.UpdateSidecar(ticket)
require.NoError(t, err)

_, err = db.SidecarBidTemplate(ticket)
require.Equal(t, ErrNoOrder, err)
}

// TestSidecarsWithSameID makes sure that sidecar tickets with the same ID but
// different offer public keys can be stored and retrieved separately.
func TestSidecarsWithSameID(t *testing.T) {
t.Parallel()

db, cleanup := newTestDB(t)
defer cleanup()

// Create a test sidecar we'll use to interact with the database.
s := &sidecar.Ticket{
ID: [8]byte{12, 34, 56},
State: sidecar.StateRegistered,
Offer: sidecar.Offer{
Capacity: 1000000,
PushAmt: 200000,
SignPubKey: testTraderKey,
LeaseDurationBlocks: 2016,
},
Recipient: &sidecar.Recipient{
MultiSigPubKey: testTraderKey,
MultiSigKeyIndex: 7,
},
}

// First, we'll add it to the database. We should be able to retrieve
// after.
require.NoError(t, db.AddSidecar(s))
assertSidecarExists(t, db, s)

// Now we'll just store it again with the same ID but with a different
// offer public key. It shouldn't overwrite the original one but add a
// second ticket.
s.Offer.SignPubKey = testBatchKey
require.NoError(t, db.AddSidecar(s))
assertSidecarExists(t, db, s)

// Just to make sure the seek of the cursor works correctly, we also add
// two more tickets, both with different IDs.
s.ID = [8]byte{99, 88, 77}
require.NoError(t, db.AddSidecar(s))
assertSidecarExists(t, db, s)
s.ID = [8]byte{55, 44, 33}
require.NoError(t, db.AddSidecar(s))
assertSidecarExists(t, db, s)

// All four should be retrievable now, but just the first two when
// querying by the ID.
all, err := db.Sidecars()
require.NoError(t, err)
require.Len(t, all, 4)

byID, err := db.SidecarsByID([8]byte{12, 34, 56})
require.NoError(t, err)
require.Len(t, byID, 2)
for _, tkt := range byID {
require.Equal(t, [8]byte{12, 34, 56}, tkt.ID)
}

// The first ticket should be the one with the test batch public key
// that we added as the second ticket since its public key is lower than
// the test public key we used for the first ticket (because of the way
// the bbolt cursor works).
require.Equal(t, testBatchKey, byID[0].Offer.SignPubKey)
require.Equal(t, testTraderKey, byID[1].Offer.SignPubKey)
}
30 changes: 0 additions & 30 deletions cmd/pool/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fmt"
Expand Down Expand Up @@ -599,35 +598,6 @@ func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl

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
}

Expand Down
80 changes: 80 additions & 0 deletions cmd/pool/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/hex"
"fmt"
"strconv"

Expand All @@ -23,6 +24,8 @@ var sidecarCommands = []cli.Command{
sidecarPrintTicketCommand,
sidecarRegisterCommand,
sidecarExpectChannelCommand,
sidecarListCommand,
sidecarCancelCommand,
},
},
}
Expand Down Expand Up @@ -291,3 +294,80 @@ func sidecarExpectChannel(ctx *cli.Context) error {

return nil
}

var sidecarListCommand = cli.Command{
Name: "list",
Aliases: []string{"l"},
Usage: "list all sidecar tickets",
Action: sidecarList,
}

func sidecarList(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() != 0 || ctx.NumFlags() != 0 {
_ = cli.ShowCommandHelp(ctx, "list")
return nil
}

client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()

resp, err := client.ListSidecars(
context.Background(), &poolrpc.ListSidecarsRequest{},
)
if err != nil {
return err
}

printRespJSON(resp)

return nil
}

var sidecarCancelCommand = cli.Command{
Name: "cancel",
Aliases: []string{"c"},
Usage: "cancel the execution of a sidecar ticket identified by " +
"its ID",
ArgsUsage: "ticket_id",
Description: `
Tries to cancel the execution of a sidecar ticket identified by its ID.
If a bid order was created for the ticket already, that order is
cancelled on the server side.`,
Action: sidecarCancel,
}

func sidecarCancel(ctx *cli.Context) error {
// Show help if no arguments or flags are provided.
if ctx.NArg() != 1 || ctx.NumFlags() != 0 {
_ = cli.ShowCommandHelp(ctx, "cancel")
return nil
}

client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()

idBytes, err := hex.DecodeString(ctx.Args().First())
if err != nil {
return fmt.Errorf("error decoding ticket ID: %v", err)
}

resp, err := client.CancelSidecar(
context.Background(), &poolrpc.CancelSidecarRequest{
SidecarId: idBytes,
},
)
if err != nil {
return err
}

printRespJSON(resp)

return nil
}
Loading

0 comments on commit 53df9a3

Please sign in to comment.