mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
335 lines
8.7 KiB
Go
335 lines
8.7 KiB
Go
package clientdb
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightninglabs/pool/account"
|
|
"github.com/lightningnetwork/lnd/tlv"
|
|
"go.etcd.io/bbolt"
|
|
)
|
|
|
|
const (
|
|
// accountStateVersionedMask is a bit mask for detecting from the state
|
|
// of an account whether that account has a version field encoded with
|
|
// it or not. We use the first bit of the uint8 state field because we
|
|
// are unlikely to ever have more than 127 different states.
|
|
accountStateVersionedMask account.State = 0b1000_0000
|
|
|
|
// accountVersionType is the first additional field we added to the
|
|
// account as a TLV field and it encodes the account's version.
|
|
accountVersionType tlv.Type = 0
|
|
)
|
|
|
|
var (
|
|
// accountBucketKey is the top level bucket where we can find all
|
|
// information about complete accounts. These accounts are indexed by
|
|
// their trader key locator.
|
|
accountBucketKey = []byte("account")
|
|
|
|
// ErrAccountNotFound is an error returned when we attempt to retrieve
|
|
// information about an account but it is not found.
|
|
ErrAccountNotFound = errors.New("account not found")
|
|
)
|
|
|
|
// isVersioned returns true if the version bit is set in the given account
|
|
// state.
|
|
func isVersioned(state account.State) bool {
|
|
return state&accountStateVersionedMask == accountStateVersionedMask
|
|
}
|
|
|
|
// setVersionBit sets the version bit in the given account state.
|
|
func setVersionBit(state account.State) account.State {
|
|
return state | accountStateVersionedMask
|
|
}
|
|
|
|
// clearVersionBit clears the version bit in the given account state.
|
|
func clearVersionBit(state account.State) account.State {
|
|
// The &^ operator means AND NOT, also known as the Bitclear operator.
|
|
return state &^ accountStateVersionedMask
|
|
}
|
|
|
|
// getAccountKey returns the key for an account which is not partial.
|
|
func getAccountKey(account *account.Account) []byte {
|
|
return account.TraderKey.PubKey.SerializeCompressed()
|
|
}
|
|
|
|
// AddAccount adds a record for the account to the database.
|
|
func (db *DB) AddAccount(account *account.Account) error {
|
|
return db.Update(func(tx *bbolt.Tx) error {
|
|
accounts, err := getBucket(tx, accountBucketKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return storeAccount(accounts, account)
|
|
})
|
|
}
|
|
|
|
// UpdateAccount updates an account in the database according to the given
|
|
// modifiers.
|
|
func (db *DB) UpdateAccount(acct *account.Account,
|
|
modifiers ...account.Modifier) error {
|
|
|
|
err := db.Update(func(tx *bbolt.Tx) error {
|
|
accounts, err := getBucket(tx, accountBucketKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountKey := getAccountKey(acct)
|
|
_, err = updateAccount(accounts, accounts, accountKey, modifiers)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, modifier := range modifiers {
|
|
modifier(acct)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateAccount reads an account from the src bucket, applies the given
|
|
// modifiers to it, and store it back into dst bucket.
|
|
func updateAccount(src, dst *bbolt.Bucket, accountKey []byte,
|
|
modifiers []account.Modifier) (*account.Account, error) {
|
|
|
|
dbAccount, err := readAccount(src, accountKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, modifier := range modifiers {
|
|
modifier(dbAccount)
|
|
}
|
|
|
|
return dbAccount, storeAccount(dst, dbAccount)
|
|
}
|
|
|
|
// Account retrieves a specific account by trader key or returns
|
|
// ErrAccountNotFound if it's not found.
|
|
func (db *DB) Account(traderKey *btcec.PublicKey) (*account.Account, error) {
|
|
var acct *account.Account
|
|
err := db.View(func(tx *bbolt.Tx) error {
|
|
accounts, err := getBucket(tx, accountBucketKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
acct, err = readAccount(
|
|
accounts, traderKey.SerializeCompressed(),
|
|
)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return acct, nil
|
|
}
|
|
|
|
// Accounts retrieves all known accounts from the database.
|
|
func (db *DB) Accounts() ([]*account.Account, error) {
|
|
var res []*account.Account
|
|
err := db.View(func(tx *bbolt.Tx) error {
|
|
accounts, err := getBucket(tx, accountBucketKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return accounts.ForEach(func(k, v []byte) error {
|
|
// We'll also get buckets here, skip those (identified
|
|
// by nil value).
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
acct, err := readAccount(accounts, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res = append(res, acct)
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func storeAccount(targetBucket *bbolt.Bucket, a *account.Account) error {
|
|
accountKey := getAccountKey(a)
|
|
|
|
var accountBuf bytes.Buffer
|
|
if err := serializeAccount(&accountBuf, a); err != nil {
|
|
return err
|
|
}
|
|
|
|
return targetBucket.Put(accountKey, accountBuf.Bytes())
|
|
}
|
|
|
|
func readAccount(sourceBucket *bbolt.Bucket,
|
|
accountKey []byte) (*account.Account, error) {
|
|
|
|
accountBytes := sourceBucket.Get(accountKey)
|
|
if accountBytes == nil {
|
|
return nil, ErrAccountNotFound
|
|
}
|
|
|
|
return deserializeAccount(bytes.NewReader(accountBytes))
|
|
}
|
|
|
|
func serializeAccount(w *bytes.Buffer, a *account.Account) error {
|
|
rawState := a.State
|
|
accountIsVersioned := a.Version > account.VersionInitialNoVersion
|
|
if accountIsVersioned {
|
|
rawState = setVersionBit(rawState)
|
|
}
|
|
|
|
err := WriteElements(
|
|
w, a.Value, a.Expiry, a.TraderKey, a.AuctioneerKey, a.BatchKey,
|
|
a.Secret, rawState, a.HeightHint, a.OutPoint,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The latest transaction is not found within StateInitiated and
|
|
// StateCanceledAfterRecovery.
|
|
switch a.State {
|
|
case account.StateInitiated, account.StateCanceledAfterRecovery:
|
|
|
|
default:
|
|
if err := WriteElement(w, a.LatestTx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// The version flag encoded within the state will inform the
|
|
// de-serialize method that it should read another field. Therefore, we
|
|
// can safely write it here.
|
|
if accountIsVersioned {
|
|
if err := serializeAccountTlvData(w, a); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// serializeAccountTlvData writes all additional TLV fields of an account to the
|
|
// given writer. This should only be called for accounts with a version > 0 as
|
|
// otherwise this will mess up the assumptions used for encoding/decoding
|
|
// accounts within a batch snapshot blob.
|
|
func serializeAccountTlvData(w *bytes.Buffer, a *account.Account) error {
|
|
version := uint8(a.Version)
|
|
tlvRecords := []tlv.Record{
|
|
tlv.MakePrimitiveRecord(accountVersionType, &version),
|
|
}
|
|
|
|
tlvStream, err := tlv.NewStream(tlvRecords...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We can't just encode the stream to the writer directly, because there
|
|
// might be multiple accounts lined up after each other. And since a TLV
|
|
// reader will always try to read until the end of a stream, we need to
|
|
// be able to cap it somehow. So we write the number of bytes and then
|
|
// the stream bytes itself.
|
|
var buf bytes.Buffer
|
|
err = tlvStream.Encode(&buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return WriteElements(w, uint32(buf.Len()), buf.Bytes())
|
|
}
|
|
|
|
// deserializeAccountTlvData reads all additional TLV fields of an account from
|
|
// the given reader. This should only be called for accounts with a version > 0
|
|
// as otherwise this will mess up the assumptions used for encoding/decoding
|
|
// accounts within a batch snapshot blob.
|
|
func deserializeAccountTlvData(r io.Reader, a *account.Account) error {
|
|
// We first need to find out how many bytes there are for this TLV
|
|
// stream and only read those bytes. Otherwise, the TLV reader will try
|
|
// to read as many bytes as it can.
|
|
var streamLen uint32
|
|
if err := ReadElement(r, &streamLen); err != nil {
|
|
return err
|
|
}
|
|
|
|
streamBytes := make([]byte, streamLen)
|
|
if err := ReadElement(r, streamBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
version uint8
|
|
)
|
|
tlvStream, err := tlv.NewStream(
|
|
tlv.MakePrimitiveRecord(accountVersionType, &version),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parsedTypes, err := tlvStream.DecodeWithParsedTypes(
|
|
bytes.NewReader(streamBytes),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if t, ok := parsedTypes[accountVersionType]; ok && t == nil {
|
|
a.Version = account.Version(version)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func deserializeAccount(r io.Reader) (*account.Account, error) {
|
|
var (
|
|
a account.Account
|
|
rawState account.State
|
|
)
|
|
err := ReadElements(
|
|
r, &a.Value, &a.Expiry, &a.TraderKey, &a.AuctioneerKey,
|
|
&a.BatchKey, &a.Secret, &rawState, &a.HeightHint, &a.OutPoint,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We might have a version flag encoded within the state. We want to
|
|
// hide that internal mechanism from the caller, so we need to remove
|
|
// the flag again.
|
|
a.State = clearVersionBit(rawState)
|
|
|
|
// The latest transaction is not found within StateInitiated and
|
|
// StateCanceledAfterRecovery.
|
|
switch a.State {
|
|
case account.StateInitiated, account.StateCanceledAfterRecovery:
|
|
|
|
default:
|
|
if err := ReadElement(r, &a.LatestTx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If there was a version flag, we know we're supposed to read another
|
|
// field here.
|
|
if isVersioned(rawState) {
|
|
if err := deserializeAccountTlvData(r, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &a, nil
|
|
}
|