diff --git a/clientdb/db.go b/clientdb/db.go index 8d01651..fd4e4cb 100644 --- a/clientdb/db.go +++ b/clientdb/db.go @@ -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 diff --git a/clientdb/sidecar.go b/clientdb/sidecar.go new file mode 100644 index 0000000..b8ff7b4 --- /dev/null +++ b/clientdb/sidecar.go @@ -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)) +} diff --git a/clientdb/sidecar_test.go b/clientdb/sidecar_test.go new file mode 100644 index 0000000..a9b7c0b --- /dev/null +++ b/clientdb/sidecar_test.go @@ -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) +} diff --git a/sidecar/interface.go b/sidecar/interface.go index e0a7f85..07ed241 100644 --- a/sidecar/interface.go +++ b/sidecar/interface.go @@ -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) +}