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) + } +}