diff --git a/account/interfaces.go b/account/interfaces.go index c4ad216..0b33858 100644 --- a/account/interfaces.go +++ b/account/interfaces.go @@ -41,6 +41,20 @@ type Reservation struct { InitialBatchKey *btcec.PublicKey } +// Version represents the version of an account. +type Version uint8 + +const ( + // VersionInitialNoVersion is the initial version any legacy account has + // that technically wasn't versioned at all. The version field isn't + // even serialized for those accounts. + VersionInitialNoVersion Version = 0 + + // VersionTaprootEnabled is the version that introduced account + // versioning and the upgrade to Taproot (with MuSig2 multi-sig). + VersionTaprootEnabled Version = 1 +) + // State describes the different possible states of an account. type State uint8 @@ -192,6 +206,9 @@ type Account struct { // NOTE: This is only nil within the StateInitiated phase. There are no // guarantees as to whether the transaction has its witness populated. LatestTx *wire.MsgTx + + // Version is the version of the account. + Version Version } const ( @@ -250,6 +267,7 @@ func (a *Account) Copy(modifiers ...Modifier) *Account { State: a.State, HeightHint: a.HeightHint, OutPoint: a.OutPoint, + Version: a.Version, } if a.State != StateInitiated { accountCopy.LatestTx = a.LatestTx.Copy() @@ -318,6 +336,14 @@ func LatestTxModifier(tx *wire.MsgTx) Modifier { } } +// VersionModifier is a functional option that modifies the version of an +// account. +func VersionModifier(version Version) Modifier { + return func(account *Account) { + account.Version = version + } +} + // Store is responsible for storing and retrieving account information reliably. type Store interface { // AddAccount adds a record for the account to the database. diff --git a/account/interfaces_test.go b/account/interfaces_test.go index fd9270d..f29ec7e 100644 --- a/account/interfaces_test.go +++ b/account/interfaces_test.go @@ -23,6 +23,7 @@ func TestAccountCopy(t *testing.T) { HeightHint: 1, OutPoint: wire.OutPoint{Index: 1}, LatestTx: wire.NewMsgTx(2), + Version: VersionTaprootEnabled, } a.Value = 2 diff --git a/clientdb/account.go b/clientdb/account.go index 71ec96f..08137e1 100644 --- a/clientdb/account.go +++ b/clientdb/account.go @@ -7,9 +7,22 @@ import ( "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 @@ -21,6 +34,23 @@ var ( 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() @@ -156,9 +186,15 @@ func readAccount(sourceBucket *bbolt.Bucket, } 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, a.State, a.HeightHint, a.OutPoint, + a.Secret, rawState, a.HeightHint, a.OutPoint, ) if err != nil { return err @@ -175,19 +211,107 @@ func serializeAccount(w *bytes.Buffer, a *account.Account) error { } } + // 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 + var ( + a account.Account + rawState account.State + ) err := ReadElements( r, &a.Value, &a.Expiry, &a.TraderKey, &a.AuctioneerKey, - &a.BatchKey, &a.Secret, &a.State, &a.HeightHint, &a.OutPoint, + &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 { @@ -199,5 +323,13 @@ func deserializeAccount(r io.Reader) (*account.Account, error) { } } + // 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 } diff --git a/clientdb/account_test.go b/clientdb/account_test.go index 3377474..502472d 100644 --- a/clientdb/account_test.go +++ b/clientdb/account_test.go @@ -130,6 +130,7 @@ func TestAccounts(t *testing.T) { a, account.StateModifier(account.StatePendingOpen), account.OutPointModifier(accountPoint), account.LatestTxModifier(accountTx), + account.VersionModifier(account.VersionTaprootEnabled), ) if err != nil { t.Fatalf("unable to update account: %v", err) diff --git a/clientdb/batch_snapshot.go b/clientdb/batch_snapshot.go index d96587d..4efe8dd 100644 --- a/clientdb/batch_snapshot.go +++ b/clientdb/batch_snapshot.go @@ -94,7 +94,7 @@ type LocalBatchSnapshot struct { MatchedOrders map[order.Nonce][]*order.MatchedOrder } -// NewSnapshots creates a new LocalBatchSnapshot from the passed order batched. +// NewSnapshot creates a new LocalBatchSnapshot from the passed order batched. func NewSnapshot(batch *order.Batch, ourOrders []order.Order, accounts []*account.Account) (*LocalBatchSnapshot, error) { diff --git a/clientdb/batch_snapshot_test.go b/clientdb/batch_snapshot_test.go index 7879b37..7831efa 100644 --- a/clientdb/batch_snapshot_test.go +++ b/clientdb/batch_snapshot_test.go @@ -27,6 +27,17 @@ var ( State: account.StateInitiated, HeightHint: 1, } + testAccountTaproot = &account.Account{ + Value: btcutil.SatoshiPerBitcoin, + Expiry: 1337, + TraderKey: testTraderKeyDesc, + AuctioneerKey: testAuctioneerKey, + BatchKey: testBatchKey, + Secret: sharedSecret, + State: account.StateInitiated, + HeightHint: 1, + Version: account.VersionTaprootEnabled, + } testNonce1 = order.Nonce([32]byte{1, 1, 1}) testNonce2 = order.Nonce([32]byte{2, 2, 2}) @@ -35,6 +46,7 @@ var ( testAccounts = map[[33]byte]*account.Account{ testRawTraderKeyArr: testAccount, + {1, 2, 3}: testAccountTaproot, } testOrders = map[order.Nonce]order.Order{ diff --git a/clientdb/codec.go b/clientdb/codec.go index 954b2b4..31d7d4d 100644 --- a/clientdb/codec.go +++ b/clientdb/codec.go @@ -56,6 +56,9 @@ func WriteElement(w *bytes.Buffer, element interface{}) error { case account.State: return lnwire.WriteElement(w, uint8(e)) + case account.Version: + return lnwire.WriteElement(w, uint8(e)) + case order.Version: return lnwire.WriteElement(w, uint32(e)) @@ -152,6 +155,13 @@ func ReadElement(r io.Reader, element interface{}) error { // nolint:gocyclo } *e = account.State(s) + case *account.Version: + var v uint8 + if err := lnwire.ReadElement(r, &v); err != nil { + return err + } + *e = account.Version(v) + case *order.Version: var v uint32 if err := lnwire.ReadElement(r, &v); err != nil {