wtxmgr: add put and fetch functions for optional transaction label

Add and test functions which can be used to write optional transaction
labels to disk in their own bucket. These labels are keyed by txid and
write the labels to disk using-length value encoding scheme. Although
the length field is not required at present, it is added to allow future
extensibility without a migration.

This approach is chosen over adding this information to txRecords,
Because a migration would be required to add a field after the variable
Length serialized tx.

The put label function will overwrite existing labels if called more
than once for the same txid. User side validation of whether we want
to override this label should be performed by calling code. Labels must
be > 0 characters and <= 500 characters (an arbitrarily chosen limit).
This commit is contained in:
carla 2020-04-30 09:08:00 +02:00
parent 4c5bc1b15d
commit 50869085eb
No known key found for this signature in database
GPG key ID: 4CA7FE54A6213C91
3 changed files with 184 additions and 0 deletions

View file

@ -62,6 +62,7 @@ var _ [32]byte = chainhash.Hash{}
var ( var (
bucketBlocks = []byte("b") bucketBlocks = []byte("b")
bucketTxRecords = []byte("t") bucketTxRecords = []byte("t")
bucketTxLabels = []byte("l")
bucketCredits = []byte("c") bucketCredits = []byte("c")
bucketUnspent = []byte("u") bucketUnspent = []byte("u")
bucketDebits = []byte("d") bucketDebits = []byte("d")

View file

@ -7,6 +7,8 @@ package wtxmgr
import ( import (
"bytes" "bytes"
"encoding/binary"
"errors"
"fmt" "fmt"
"time" "time"
@ -18,6 +20,28 @@ import (
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
) )
// TxLabelLimit is the length limit we impose on transaction labels.
const TxLabelLimit = 500
var (
// ErrEmptyLabel is returned when an attempt to write a label that is
// empty is made.
ErrEmptyLabel = errors.New("empty transaction label not allowed")
// ErrLabelTooLong is returned when an attempt to write a label that is
// to long is made.
ErrLabelTooLong = errors.New("transaction label exceeds limit")
// ErrNoLabelBucket is returned when the bucket holding optional
// transaction labels is not found. This occurs when no transactions
// have been labelled yet.
ErrNoLabelBucket = errors.New("labels bucket does not exist")
// ErrTxLabelNotFound is returned when no label is found for a
// transaction hash.
ErrTxLabelNotFound = errors.New("label for transaction not found")
)
// Block contains the minimum amount of data to uniquely identify any block on // Block contains the minimum amount of data to uniquely identify any block on
// either the best or side chain. // either the best or side chain.
type Block struct { type Block struct {
@ -935,3 +959,70 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32)
return bal, nil return bal, nil
} }
// PutTxLabel validates transaction labels and writes them to disk if they
// are non-zero and within the label length limit. The entry is keyed by the
// transaction hash:
// [0:32] Transaction hash (32 bytes)
//
// The label itself is written to disk in length value format:
// [0:2] Label length
// [2: +len] Label
func (s *Store) PutTxLabel(ns walletdb.ReadWriteBucket, txid chainhash.Hash,
label string) error {
if len(label) == 0 {
return ErrEmptyLabel
}
if len(label) > TxLabelLimit {
return ErrLabelTooLong
}
labelBucket, err := ns.CreateBucketIfNotExists(bucketTxLabels)
if err != nil {
return err
}
// We expect the label length to be limited on creation, so we can
// store the label's length as a uint16.
labelLen := uint16(len(label))
var buf bytes.Buffer
var b [2]byte
binary.BigEndian.PutUint16(b[:], labelLen)
if _, err := buf.Write(b[:]); err != nil {
return err
}
if _, err := buf.WriteString(label); err != nil {
return err
}
return labelBucket.Put(txid[:], buf.Bytes())
}
// FetchTxLabel reads a transaction label from the tx labels bucket. If a label
// with 0 length was written, we return an error, since this is unexpected.
func FetchTxLabel(ns walletdb.ReadBucket, txid chainhash.Hash) (string, error) {
labelBucket := ns.NestedReadBucket(bucketTxLabels)
if labelBucket == nil {
return "", ErrNoLabelBucket
}
v := labelBucket.Get(txid[:])
if v == nil {
return "", ErrTxLabelNotFound
}
// If the label is empty, return an error.
length := binary.BigEndian.Uint16(v[0:2])
if length == 0 {
return "", ErrEmptyLabel
}
// Read the remainder of the bytes into a label string.
label := string(v[2:])
return label, nil
}

View file

@ -2263,3 +2263,95 @@ func TestInsertMempoolTxAndConfirm(t *testing.T) {
} }
}) })
} }
// TestTxLabel tests reading and writing of transaction labels.
func TestTxLabel(t *testing.T) {
t.Parallel()
store, db, teardown, err := testStore()
if err != nil {
t.Fatal(err)
}
defer teardown()
// txid is the transaction hash we will use to write and get labels for.
txid := TstRecvTx.Hash()
// txidNotFound is distinct from txid, and will not have a label written
// to disk.
txidNotFound := TstSpendingTx.Hash()
// getBucket gets the top level bucket, and fails the test if it is
// not found.
getBucket := func(tx walletdb.ReadWriteTx) walletdb.ReadWriteBucket {
testBucket := tx.ReadWriteBucket(namespaceKey)
if testBucket == nil {
t.Fatalf("could not get bucket: %v", err)
}
return testBucket
}
// tryPutLabel attempts to write a label to disk.
tryPutLabel := func(label string) error {
return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
// Try to write the label to disk.
return store.PutTxLabel(getBucket(tx), *txid, label)
})
}
// tryReadLabel attempts to retrieve a label for a given txid.
tryReadLabel := func(labelTx chainhash.Hash) (string, error) {
var label string
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
var err error
label, err = FetchTxLabel(getBucket(tx), labelTx)
return err
})
return label, err
}
// First, try to lookup a label when the labels bucket does not exist
// yet.
_, err = tryReadLabel(*txid)
if err != ErrNoLabelBucket {
t.Fatalf("expected: %v, got: %v", ErrNoLabelBucket, err)
}
// Now try to write an empty label, which should fail.
err = tryPutLabel("")
if err != ErrEmptyLabel {
t.Fatalf("expected: %v, got: %v", ErrEmptyLabel, err)
}
// Create a label which exceeds the length limit.
longLabel := make([]byte, TxLabelLimit+1)
err = tryPutLabel(string(longLabel))
if err != ErrLabelTooLong {
t.Fatalf("expected: %v, got: %v", ErrLabelTooLong, err)
}
// Write an acceptable length label to disk, this should succeed.
testLabel := "test label"
err = tryPutLabel(testLabel)
if err != nil {
t.Fatalf("expected: no error, got: %v", err)
}
diskLabel, err := tryReadLabel(*txid)
if err != nil {
t.Fatalf("expected: no error, got: %v", err)
}
if diskLabel != testLabel {
t.Fatalf("expected: %v, got: %v", testLabel, diskLabel)
}
// Finally, try to read a label for a transaction which does not have
// one.
_, err = tryReadLabel(*txidNotFound)
if err != ErrTxLabelNotFound {
t.Fatalf("expected: %v, got: %v", ErrTxLabelNotFound, err)
}
}