From 50869085ebb41b843dc014a17b7c9480c1de9aff Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 30 Apr 2020 09:08:00 +0200 Subject: [PATCH] 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). --- wtxmgr/db.go | 1 + wtxmgr/tx.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++ wtxmgr/tx_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) diff --git a/wtxmgr/db.go b/wtxmgr/db.go index 28839a6..0a90f44 100644 --- a/wtxmgr/db.go +++ b/wtxmgr/db.go @@ -62,6 +62,7 @@ var _ [32]byte = chainhash.Hash{} var ( bucketBlocks = []byte("b") bucketTxRecords = []byte("t") + bucketTxLabels = []byte("l") bucketCredits = []byte("c") bucketUnspent = []byte("u") bucketDebits = []byte("d") diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index fbf0427..512fe1a 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -7,6 +7,8 @@ package wtxmgr import ( "bytes" + "encoding/binary" + "errors" "fmt" "time" @@ -18,6 +20,28 @@ import ( "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 // either the best or side chain. type Block struct { @@ -935,3 +959,70 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) 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 +} diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 84a6023..e2f2c2a 100644 --- a/wtxmgr/tx_test.go +++ b/wtxmgr/tx_test.go @@ -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) + } +}