mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
397 lines
9.9 KiB
Go
397 lines
9.9 KiB
Go
package clientdb
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightninglabs/pool/order"
|
|
"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")
|
|
|
|
// bidTemplateBucket is a bucket that's used to store the order
|
|
// template of a sidecar ticket for the provider to be able to execute
|
|
// automated negotiation of the order.
|
|
bidTemplateBucket = []byte("sidecar-bids")
|
|
)
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
// AddSidecarWithBid is identical to the AddSidecar method, but it also inserts
|
|
// a bid template in a special bucket to facilitate automated negotiation of
|
|
// sidecar channels.
|
|
func (db *DB) AddSidecarWithBid(ticket *sidecar.Ticket, bid *order.Bid) error {
|
|
sidecarKey, err := getSidecarKey(ticket.ID, ticket.Offer.SignPubKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Although the order hasn't fully advanced set to the state where we
|
|
// sign+commit to the order nonce itself, since we already know it at
|
|
// this point, we can just apply it directly to the ticket.
|
|
ticket.Order = new(sidecar.Order)
|
|
bidNonce := bid.Nonce()
|
|
copy(ticket.Order.BidNonce[:], bidNonce[:])
|
|
|
|
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)
|
|
}
|
|
|
|
err = storeSidecar(sidecarBucket, sidecarKey, ticket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bidBucket, err := sidecarBucket.CreateBucketIfNotExists(
|
|
bidTemplateBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return storeBidTemplate(bidBucket, bid, bidNonce)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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) {
|
|
var bid *order.Bid
|
|
|
|
err := db.View(func(tx *bbolt.Tx) error {
|
|
sidecarBucket, err := getBucket(tx, sidecarsBucketKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bidBucket := sidecarBucket.Bucket(bidTemplateBucket)
|
|
if bidBucket == nil {
|
|
return err
|
|
}
|
|
|
|
bid, err = readBidTemplate(bidBucket, ticket.Order.BidNonce)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bid, 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 {
|
|
// 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 v == nil {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// TODO(roasbeef): store bid along side in new key?
|
|
|
|
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))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
err := storeOrderTX(bidBucket, ticketNonce, w.Bytes(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = storeOrderMinUnitsMatchTX(
|
|
bidBucket, ticketNonce, bid.Details().MinUnitsMatch,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := storeOrderTlvTX(bidBucket, ticketNonce, bid); err != nil {
|
|
return err
|
|
}
|
|
return storeOrderMinNoderTierTX(bidBucket, ticketNonce, bid.MinNodeTier)
|
|
}
|
|
|
|
func readBidTemplate(bidBucket *bbolt.Bucket,
|
|
ticketNonce order.Nonce) (*order.Bid, error) {
|
|
|
|
var (
|
|
o order.Order
|
|
err error
|
|
)
|
|
|
|
callback := func(nonce order.Nonce, rawOrder []byte,
|
|
extraData *extraOrderData) error {
|
|
|
|
r := bytes.NewReader(rawOrder)
|
|
o, err = DeserializeOrder(nonce, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tlvReader := bytes.NewReader(extraData.tlvData)
|
|
err := deserializeOrderTlvData(tlvReader, o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if bidOrder, ok := o.(*order.Bid); ok {
|
|
bidOrder.MinNodeTier = extraData.minNodeTier
|
|
}
|
|
o.Details().MinUnitsMatch = extraData.minUnitsMatch
|
|
|
|
return nil
|
|
}
|
|
|
|
err = fetchOrderTX(bidBucket, ticketNonce, callback)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|