pool/clientdb/batch_snapshot.go
Matt Morehouse 8b239fab7e
clientdb: move WriteElement[s] to own package
Move WriteElement[s] to the codec package so that packages other than
clientdb can use it.

For the codec package, we avoid depending on other packages within Pool
so that no circular dependencies develop. This requires us to move some
of the uintXX casting logic in clientdb/codec.go to call sites.
2023-05-16 17:30:59 -05:00

709 lines
17 KiB
Go

package clientdb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/codec"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/terms"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"go.etcd.io/bbolt"
)
// batch-snapshot-bucket
//
// |
// |-- batch-snapshot-pending-key: <batch snapshot>
// |
// |-- batch-snapshot-seq-bucket
// | |
// | |-- <sequence num>
// | | |
// | | |-- batch-snapshot-batch: <batch snapshot>
// | |
// | |-- <sequence num>
// | | |
// | ... ...
// |
// |-- batch-snapshot-batchid-index-bucket
// |
// |-- <batch id>: <sequence-num>
// |-- <batch id>: <sequence-num>
// |
// ...
var (
// batchSnapshotBucketKey is the top level bucket where we'll find
// snapshot information about all batches we have participated in.
batchSnapshotBucketKey = []byte("batch-snapshot-bucket")
// batchSnapshotPendingKey is a key where we will store the snapshot
// for a pending batch. When the batch has been finalizd we'll move it
// into a sub-bucket for long-term record keeping.
batchSnapshotPendingKey = []byte("batch-snapshot-pending")
// batchSnapshotSeqBucketKey is a sub-bucket where we'll store batch
// snapshots indexed by sequence number.
batchSnapshotSeqBucketKey = []byte("batch-snapshot-seq-bucket")
// batchSnapshotBatchIDIndexBucket is a sub-bucket where we'll map
// batch IDs to their sequence number.
batchSnapshotBatchIDIndexBucketKey = []byte("batch-snapshot-batchid-index-bucket")
// batchSnapshotBatchKey is the key under where we'll store the
// serialized batch snapshot.
batchSnapshotBatchKey = []byte("batch-snapshot-batch")
)
// LocalBatchSnapshot holds key information about our participation in a batch.
type LocalBatchSnapshot struct {
// Version is the version of the batch verification protocol.
Version order.BatchVersion
// BatchID is the batch's unique ID.
BatchID order.BatchID
// ClearingPrices is a map of the lease duration markets and the fixed
// rate the orders were cleared at within that market.
ClearingPrices map[uint32]order.FixedRatePremium
// ExecutionFee is the FeeSchedule that was used by the server to
// calculate the execution fee.
ExecutionFee terms.LinearFeeSchedule
// BatchTX is the complete batch transaction with all non-witness data
// fully populated.
BatchTX *wire.MsgTx
// BatchTxFeeRate is the miner fee rate in sat/kW that was chosen for
// the batch transaction.
BatchTxFeeRate chainfee.SatPerKWeight
// Account holds snapshots of the ending state of the local accounts
// that participated in this batch.
Accounts map[[33]byte]*account.Account
// Orders holds snapshots of the ending state of local orders that were
// part of matches in this batch.
Orders map[order.Nonce]order.Order
// MatchedOrders is a map between all trader's orders and the other
// orders that were matched to them in the batch.
MatchedOrders map[order.Nonce][]*order.MatchedOrder
}
// NewSnapshot creates a new LocalBatchSnapshot from the passed order batched.
func NewSnapshot(batch *order.Batch, ourOrders []order.Order,
accounts []*account.Account) (*LocalBatchSnapshot, error) {
// We only support LinearFeeSchedule at this point (because of
// serialization).
feeSched, ok := batch.ExecutionFee.(*terms.LinearFeeSchedule)
if !ok {
return nil, fmt.Errorf("unsupported fee schedule: %T",
batch.ExecutionFee)
}
as := make(map[[33]byte]*account.Account)
for _, a := range accounts {
var key [33]byte
copy(key[:], a.TraderKey.PubKey.SerializeCompressed())
as[key] = a
}
os := make(map[order.Nonce]order.Order)
for _, o := range ourOrders {
os[o.Nonce()] = o
}
snapshot := &LocalBatchSnapshot{
Version: batch.Version,
BatchID: batch.ID,
ClearingPrices: batch.ClearingPrices,
ExecutionFee: *feeSched,
BatchTX: batch.BatchTX,
BatchTxFeeRate: batch.BatchTxFeeRate,
Accounts: as,
Orders: os,
MatchedOrders: batch.MatchedOrders,
}
return snapshot, nil
}
// GetLocalBatchSnapshots returns snapshots for all batches the trader has
// participated in.
func (db *DB) GetLocalBatchSnapshots() ([]*LocalBatchSnapshot, error) {
var snapshots []*LocalBatchSnapshot
err := db.View(func(tx *bbolt.Tx) error {
var err error
snapshots, err = db.fetchLocalBatchSnapshots(tx)
return err
})
if err != nil {
return nil, err
}
return snapshots, nil
}
// GetLocalBatchSnapshot fetches the batch snapshot for the given batch ID.
func (db *DB) GetLocalBatchSnapshot(id order.BatchID) (
*LocalBatchSnapshot, error) {
var snapshot *LocalBatchSnapshot
err := db.View(func(tx *bbolt.Tx) error {
_, seqBucket, indexBucket, err := getSnapshotBuckets(tx)
if err != nil {
return err
}
seq := indexBucket.Get(id[:])
if seq == nil {
return fmt.Errorf("snapshot of batch %x not found",
id[:])
}
rootOrderBucket, err := getBucket(tx, ordersBucketKey)
if err != nil {
return err
}
snapshot, err = fetchLocalBatchSnapshot(
seqBucket, seq, rootOrderBucket,
)
return err
})
if err != nil {
return nil, err
}
return snapshot, nil
}
func (db *DB) fetchLocalBatchSnapshots(tx *bbolt.Tx) ([]*LocalBatchSnapshot,
error) {
_, seqBucket, _, err := getSnapshotBuckets(tx)
if err != nil {
return nil, err
}
rootOrderBucket, err := getBucket(tx, ordersBucketKey)
if err != nil {
return nil, err
}
// Each entry in the top-level bucket is a sub-bucket index by the
// sequence number.
var snapshots []*LocalBatchSnapshot
err = seqBucket.ForEach(func(seq, v []byte) error {
batchSnapshot, err := fetchLocalBatchSnapshot(
seqBucket, seq, rootOrderBucket,
)
if err != nil {
return err
}
// Add this batch to our list of snapshots.
snapshots = append(snapshots, batchSnapshot)
return nil
})
if err != nil {
return nil, err
}
return snapshots, nil
}
func fetchLocalBatchSnapshot(seqBucket *bbolt.Bucket, seqNum []byte,
rootOrderBucket *bbolt.Bucket) (*LocalBatchSnapshot, error) {
snapshotBucket, err := getNestedBucket(seqBucket, seqNum, false)
if err != nil {
return nil, err
}
// Get the serialized batch.
rawBatch := snapshotBucket.Get(batchSnapshotBatchKey)
if rawBatch == nil {
return nil, fmt.Errorf("batch not found for snapshot")
}
// Deserialize it.
batchSnapshot, err := deserializeLocalBatchSnapshot(
bytes.NewReader(rawBatch),
)
if err != nil {
return nil, err
}
// The snapshot doesn't contain all the order information needed, so
// we'll retrieve the missing data.
for nonce, o := range batchSnapshot.Orders {
orderBucket, err := getNestedBucket(
rootOrderBucket, nonce[:], false,
)
if err != nil {
return nil, ErrNoOrder
}
minUnitsMatchBytes := orderBucket.Get(orderMinUnitsMatchKey)
if minUnitsMatchBytes == nil {
// Assume a base unit minimum match for older orders
// which were not aware of the field.
o.Details().MinUnitsMatch = 1
} else {
var minUnitsMatch order.SupplyUnit
err := ReadElement(
bytes.NewReader(minUnitsMatchBytes),
&minUnitsMatch,
)
if err != nil {
return nil, err
}
o.Details().MinUnitsMatch = minUnitsMatch
}
tlvBytes := orderBucket.Get(orderTlvKey)
if tlvBytes != nil {
r := bytes.NewReader(tlvBytes)
if err := deserializeOrderTlvData(r, o); err != nil {
return nil, err
}
}
// We'll only need to populate the values below for bid orders.
bidOrder, ok := o.(*order.Bid)
if !ok {
continue
}
minNodeTierBytes := orderBucket.Get(orderTierKey)
if minNodeTierBytes == nil {
// If not found, then assume the current default value.
bidOrder.MinNodeTier = order.DefaultMinNodeTier
} else {
var minNodeTier order.NodeTier
err := ReadElement(
bytes.NewReader(minNodeTierBytes),
&minNodeTier,
)
if err != nil {
return nil, err
}
bidOrder.MinNodeTier = minNodeTier
}
}
return batchSnapshot, nil
}
func storePendingBatchSnapshot(tx *bbolt.Tx,
snapshot *LocalBatchSnapshot) error {
topBucket, err := getBucket(tx, batchSnapshotBucketKey)
if err != nil {
return err
}
buf := bytes.Buffer{}
if err := serializeLocalBatchSnapshot(&buf, snapshot); err != nil {
return err
}
// Store the batch under the pending key, we'll move it when it is
// finalized.
return topBucket.Put(batchSnapshotPendingKey, buf.Bytes())
}
// fetchPendingBatchSnapshot retrieves the currently pending batch snapshot from
// the database or returns the account.ErrNoPendingBatch error if none exists.
func fetchPendingBatchSnapshot(tx *bbolt.Tx) (*LocalBatchSnapshot, error) {
topBucket, err := getBucket(tx, batchSnapshotBucketKey)
if err != nil {
return nil, err
}
snapshotBytes := topBucket.Get(batchSnapshotPendingKey)
if len(snapshotBytes) == 0 {
return nil, account.ErrNoPendingBatch
}
return deserializeLocalBatchSnapshot(bytes.NewReader(snapshotBytes))
}
// finalizeBatchSnapshot moves the pending batch snapshot into the sub-bucket
// indexed by sequence numbers.
func finalizeBatchSnapshot(tx *bbolt.Tx, batchID order.BatchID) error {
topBucket, seqBucket, indexBucket, err := getSnapshotBuckets(tx)
if err != nil {
return err
}
rawSnapshot := topBucket.Get(batchSnapshotPendingKey)
if rawSnapshot == nil {
return fmt.Errorf("pending snapshot not found")
}
err = topBucket.Delete(batchSnapshotPendingKey)
if err != nil {
return err
}
// Get the next sequence number we will store this batch under.
sequence, err := seqBucket.NextSequence()
if err != nil {
return err
}
var seqBytes [8]byte
binary.BigEndian.PutUint64(seqBytes[:], sequence)
// Create a sub-bucket for this sequence number.
snapshotBucket, err := getNestedBucket(seqBucket, seqBytes[:], true)
if err != nil {
return err
}
err = snapshotBucket.Put(batchSnapshotBatchKey, rawSnapshot)
if err != nil {
return err
}
// Finally add a entry mapping this batch ID to the sequence number.
err = indexBucket.Put(batchID[:], seqBytes[:])
if err != nil {
return err
}
return nil
}
func getSnapshotBuckets(tx *bbolt.Tx) (*bbolt.Bucket, *bbolt.Bucket,
*bbolt.Bucket, error) {
topBucket, err := getBucket(tx, batchSnapshotBucketKey)
if err != nil {
return nil, nil, nil, err
}
// Get the sub-buckets. We expect them to be created at DB init, so we
// don't attempt to create them if non-existent.
seqBucket, err := getNestedBucket(
topBucket, batchSnapshotSeqBucketKey, false,
)
if err != nil {
return nil, nil, nil, err
}
indexBucket, err := getNestedBucket(
topBucket, batchSnapshotBatchIDIndexBucketKey, false,
)
if err != nil {
return nil, nil, nil, err
}
return topBucket, seqBucket, indexBucket, nil
}
func serializeLocalBatchSnapshot(w *bytes.Buffer, b *LocalBatchSnapshot) error {
// The previous batch versions had a single clearing price but because
// we now always store the price map afterwards, we signal a new batch
// by storing an explicit zero price.
var zeroPrice order.FixedRatePremium
err := codec.WriteElements(
w, uint32(b.Version), b.BatchID[:], uint32(zeroPrice),
uint64(b.ExecutionFee.BaseFee()),
uint64(b.ExecutionFee.FeeRate()),
b.BatchTX, b.BatchTxFeeRate,
)
if err != nil {
return err
}
if err := serializeAccounts(w, b.Accounts); err != nil {
return err
}
if err := serializeOrders(w, b.Orders); err != nil {
return err
}
type match struct {
nonce order.Nonce
match *order.MatchedOrder
}
var matchedOrders []*match
for nonce, matches := range b.MatchedOrders {
for _, m := range matches {
matchedOrders = append(matchedOrders, &match{
nonce: nonce,
match: m,
})
}
}
numMatches := uint32(len(matchedOrders))
err = codec.WriteElements(w, numMatches)
if err != nil {
return err
}
for _, m := range matchedOrders {
err := serializeMatchedOrder(w, m.nonce, m.match)
if err != nil {
return err
}
}
// New batch versions have an additional map of duration->price that we
// need to serialize. Since both values are uint32 this is pretty
// straightforward.
numPrices := uint32(len(b.ClearingPrices))
err = codec.WriteElements(w, numPrices)
if err != nil {
return err
}
for duration, price := range b.ClearingPrices {
err = codec.WriteElements(w, duration, uint32(price))
if err != nil {
return err
}
}
return nil
}
func deserializeLocalBatchSnapshot(r io.Reader) (*LocalBatchSnapshot, error) {
b := &LocalBatchSnapshot{
ClearingPrices: make(map[uint32]order.FixedRatePremium),
}
var clearingPrice order.FixedRatePremium
err := ReadElements(
r, &b.Version, b.BatchID[:], &clearingPrice, &b.ExecutionFee,
&b.BatchTX, &b.BatchTxFeeRate,
)
if err != nil {
return nil, err
}
b.Accounts, err = deserializeAccounts(r)
if err != nil {
return nil, err
}
b.Orders, err = deserializeOrders(r)
if err != nil {
return nil, err
}
var numMatches uint32
err = ReadElements(r, &numMatches)
if err != nil {
return nil, err
}
b.MatchedOrders = make(map[order.Nonce][]*order.MatchedOrder)
for i := uint32(0); i < numMatches; i++ {
nonce, m, err := deserializeMatchedOrder(r)
if err != nil {
return nil, err
}
b.MatchedOrders[nonce] = append(b.MatchedOrders[nonce], m)
}
// Older batches had a single clearing price instead of a map. If we
// have a non-zero price, it means this is an old snapshot and we don't
// need to read any further.
if clearingPrice > 0 {
b.ClearingPrices[order.LegacyLeaseDurationBucket] = clearingPrice
return b, nil
}
var numPrices uint32
err = ReadElements(r, &numPrices)
if err != nil {
return nil, err
}
for i := uint32(0); i < numPrices; i++ {
var (
price order.FixedRatePremium
duration uint32
)
err = ReadElements(r, &duration, &price)
if err != nil {
return nil, err
}
b.ClearingPrices[duration] = price
}
return b, nil
}
func serializeAccounts(w *bytes.Buffer,
accounts map[[33]byte]*account.Account) error {
err := codec.WriteElements(w, uint32(len(accounts)))
if err != nil {
return err
}
for key, a := range accounts {
// Write key, then account.
err := codec.WriteElements(w, key)
if err != nil {
return err
}
err = serializeAccount(w, a)
if err != nil {
return err
}
}
return nil
}
func deserializeAccounts(r io.Reader) (map[[33]byte]*account.Account, error) {
var numAccounts uint32
err := ReadElements(r, &numAccounts)
if err != nil {
return nil, err
}
accs := make(map[[33]byte]*account.Account)
for i := uint32(0); i < numAccounts; i++ {
var key [33]byte
err := ReadElements(r, &key)
if err != nil {
return nil, err
}
a, err := deserializeAccount(r)
if err != nil {
return nil, err
}
accs[key] = a
}
return accs, nil
}
func serializeOrders(w *bytes.Buffer, orders map[order.Nonce]order.Order) error {
err := codec.WriteElements(w, uint32(len(orders)))
if err != nil {
return err
}
for nonce, o := range orders {
err := codec.WriteElements(w, nonce[:])
if err != nil {
return err
}
err = SerializeOrder(o, w)
if err != nil {
return err
}
}
return nil
}
func deserializeOrders(r io.Reader) (map[order.Nonce]order.Order, error) {
var numOrders uint32
err := ReadElements(r, &numOrders)
if err != nil {
return nil, err
}
orders := make(map[order.Nonce]order.Order)
for i := uint32(0); i < numOrders; i++ {
var nonce order.Nonce
err := ReadElements(r, &nonce)
if err != nil {
return nil, err
}
o, err := DeserializeOrder(nonce, r)
if err != nil {
return nil, err
}
orders[nonce] = o
}
return orders, nil
}
func serializeMatchedOrder(w *bytes.Buffer, ourNonce order.Nonce,
m *order.MatchedOrder) error {
orderNonce := m.Order.Nonce()
err := codec.WriteElements(w, ourNonce[:], orderNonce[:])
if err != nil {
return err
}
err = SerializeOrder(m.Order, w)
if err != nil {
return err
}
err = codec.WriteElements(
w, m.MultiSigKey, m.NodeKey, m.NodeAddrs,
uint64(m.UnitsFilled),
)
if err != nil {
return err
}
return nil
}
func deserializeMatchedOrder(r io.Reader) (order.Nonce,
*order.MatchedOrder, error) {
var ourNonce, theirNonce order.Nonce
err := ReadElements(r, &ourNonce, &theirNonce)
if err != nil {
return order.Nonce{}, nil, err
}
m := &order.MatchedOrder{}
o, err := DeserializeOrder(theirNonce, r)
if err != nil {
return order.Nonce{}, nil, err
}
m.Order = o
err = ReadElements(
r, &m.MultiSigKey, &m.NodeKey, &m.NodeAddrs,
&m.UnitsFilled,
)
if err != nil {
return order.Nonce{}, nil, err
}
return ourNonce, m, nil
}
// deletePendingSnapshot deletes the pending batch snapshot.
func deletePendingSnapshot(tx *bbolt.Tx) error {
topBucket, err := getBucket(tx, batchSnapshotBucketKey)
if err != nil {
return err
}
return topBucket.Delete(batchSnapshotPendingKey)
}