sidecar+clientdb: add sidecar storage

Both the trader offering to buy a sidecar channel for another node as
well as the recipient node will need to store the involved sidecar
tickets in their local databases. We add a simple store interface and
its implementation that will be used in future commits.
This commit is contained in:
Oliver Gugger 2021-03-19 11:50:02 +01:00
parent 363ddd5a8e
commit f84debaeff
No known key found for this signature in database
GPG key ID: 8E4256593F177720
4 changed files with 267 additions and 0 deletions

View file

@ -97,6 +97,10 @@ func initDB(filepath string, firstInit bool) (*bbolt.DB, error) {
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(sidecarsBucketKey)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(batchBucketKey)
if err != nil {
return err

173
clientdb/sidecar.go Normal file
View file

@ -0,0 +1,173 @@
package clientdb
import (
"bytes"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/lightninglabs/pool/sidecar"
"go.etcd.io/bbolt"
)
var (
// ErrNoSidecar is the error returned if no sidecar with the given
// multisig pubkey exists in the store.
ErrNoSidecar = errors.New("no sidecar found")
// sidecarsBucketKey is a bucket that contains all sidecars that are
// currently pending or completed. This bucket is keyed by the ticket ID
// and offer signing pubkey of a sidecar.
sidecarsBucketKey = []byte("sidecars")
)
const (
// sidecarKeyLen is the length of a sidecar ticket's key. It is the
// length of the sidecar ID (8 bytes) plus the length of a compressed
// public key (33 bytes).
sidecarKeyLen = 8 + 33
)
// A compile time check to make sure we satisfy the sidecar.Store interface.
var _ sidecar.Store = (*DB)(nil)
// getSidecarKey returns the key for a sidecar.
func getSidecarKey(id [8]byte, offerSignPubKey *btcec.PublicKey) ([]byte,
error) {
if offerSignPubKey == nil {
return nil, fmt.Errorf("offer signing pubkey cannot be nil")
}
var result [sidecarKeyLen]byte
copy(result[:], id[:])
copy(result[8:], offerSignPubKey.SerializeCompressed())
return result[:], nil
}
// AddSidecar adds a record for the sidecar to the database.
func (db *DB) AddSidecar(ticket *sidecar.Ticket) error {
sidecarKey, err := getSidecarKey(ticket.ID, ticket.Offer.SignPubKey)
if err != nil {
return err
}
return db.Update(func(tx *bbolt.Tx) error {
sidecarBucket, err := getBucket(tx, sidecarsBucketKey)
if err != nil {
return err
}
sidecarValue := sidecarBucket.Get(sidecarKey)
if len(sidecarValue) != 0 {
return fmt.Errorf("sidecar for key %x already exists",
sidecarKey)
}
return storeSidecar(sidecarBucket, sidecarKey, ticket)
})
}
// UpdateSidecar updates a sidecar in the database.
func (db *DB) UpdateSidecar(ticket *sidecar.Ticket) error {
sidecarKey, err := getSidecarKey(ticket.ID, ticket.Offer.SignPubKey)
if err != nil {
return err
}
return db.Update(func(tx *bbolt.Tx) error {
sidecarBucket, err := getBucket(tx, sidecarsBucketKey)
if err != nil {
return err
}
sidecarValue := sidecarBucket.Get(sidecarKey)
if len(sidecarValue) == 0 {
return ErrNoSidecar
}
return storeSidecar(sidecarBucket, sidecarKey, ticket)
})
}
// Sidecar retrieves a specific sidecar by its ID and provider signing key
// (offer signature pubkey) or returns ErrNoSidecar if it's not found.
func (db *DB) Sidecar(id [8]byte,
offerSignPubKey *btcec.PublicKey) (*sidecar.Ticket, error) {
sidecarKey, err := getSidecarKey(id, offerSignPubKey)
if err != nil {
return nil, err
}
var s *sidecar.Ticket
err = db.View(func(tx *bbolt.Tx) error {
sidecarBucket, err := getBucket(tx, sidecarsBucketKey)
if err != nil {
return err
}
s, err = readSidecar(sidecarBucket, sidecarKey)
return err
})
if err != nil {
return nil, err
}
return s, nil
}
// Sidecars retrieves all known sidecars from the database.
func (db *DB) Sidecars() ([]*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
}
return sidecarBucket.ForEach(func(k, v []byte) error {
// We don't expect any sub-buckets with sidecars.
if v == nil {
return fmt.Errorf("nil value for key %x", k)
}
s, err := readSidecar(sidecarBucket, k)
if err != nil {
return err
}
res = append(res, s)
return nil
})
})
if err != nil {
return nil, err
}
return res, nil
}
func storeSidecar(targetBucket *bbolt.Bucket, key []byte,
ticket *sidecar.Ticket) error {
var sidecarBuf bytes.Buffer
if err := sidecar.SerializeTicket(&sidecarBuf, ticket); err != nil {
return err
}
return targetBucket.Put(key, sidecarBuf.Bytes())
}
func readSidecar(sourceBucket *bbolt.Bucket, id []byte) (*sidecar.Ticket,
error) {
sidecarBytes := sourceBucket.Get(id)
if sidecarBytes == nil {
return nil, ErrNoSidecar
}
return sidecar.DeserializeTicket(bytes.NewReader(sidecarBytes))
}

72
clientdb/sidecar_test.go Normal file
View file

@ -0,0 +1,72 @@
package clientdb
import (
"testing"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/sidecar"
"github.com/stretchr/testify/require"
)
func assertSidecarExists(t *testing.T, db *DB, expected *sidecar.Ticket) {
t.Helper()
found, err := db.Sidecar(expected.ID, expected.Offer.SignPubKey)
require.NoError(t, err)
require.Equal(t, expected, found)
}
// TestSidecars ensures that all database operations involving sidecars run as
// expected.
func TestSidecars(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.
err := db.AddSidecar(s)
require.NoError(t, err)
assertSidecarExists(t, db, s)
// Transition the sidecar state from SidecarInitialized to
// SidecarExpectingChannel and add the required information for that
// state.
s.State = sidecar.StateExpectingChannel
s.Order = &sidecar.Order{
BidNonce: order.Nonce{1, 2, 3},
}
err = db.UpdateSidecar(s)
require.NoError(t, err)
assertSidecarExists(t, db, s)
// Retrieving all sidecars should show that we only have one sidecar,
// the same one.
sidecars, err := db.Sidecars()
require.NoError(t, err)
require.Len(t, sidecars, 1)
require.Contains(t, sidecars, s)
// Make sure we can query a sidecar ticket by its ID and offer pubkey.
updatedTicket, err := db.Sidecar([8]byte{12, 34, 56}, testTraderKey)
require.NoError(t, err)
require.Equal(t, s, updatedTicket)
}

View file

@ -264,3 +264,21 @@ func (t *Ticket) OrderDigest() ([32]byte, error) {
}
return sha256.Sum256(msg.Bytes()), nil
}
// Store is the interface a persistent storage must implement for storing and
// retrieving sidecar tickets.
type Store interface {
// AddSidecar adds a record for the sidecar order to the database.
AddSidecar(sidecar *Ticket) error
// UpdateSidecar updates a sidecar order in the database.
UpdateSidecar(sidecar *Ticket) error
// Sidecar retrieves a specific sidecar by its ID and provider signing
// key (offer signature pubkey) or returns ErrNoSidecar if it's not
// found.
Sidecar(id [8]byte, offerSignPubKey *btcec.PublicKey) (*Ticket, error)
// Sidecars retrieves all known sidecar orders from the database.
Sidecars() ([]*Ticket, error)
}