Merge pull request #696 from carlaKC/txn-labels
wtxmgr: add labels to transactions
This commit is contained in:
commit
b07494fc2d
11 changed files with 484 additions and 80 deletions
|
@ -1378,7 +1378,7 @@ func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount,
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb)
|
||||
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb, "")
|
||||
if err != nil {
|
||||
if err == txrules.ErrAmountNegative {
|
||||
return "", ErrNeedPositiveAmount
|
||||
|
|
|
@ -519,7 +519,7 @@ func (s *walletServer) PublishTransaction(ctx context.Context, req *pb.PublishTr
|
|||
"Bytes do not represent a valid raw transaction: %v", err)
|
||||
}
|
||||
|
||||
err = s.wallet.PublishTransaction(&msgTx)
|
||||
err = s.wallet.PublishTransaction(&msgTx, "")
|
||||
if err != nil {
|
||||
return nil, translateError(err)
|
||||
}
|
||||
|
|
|
@ -6,16 +6,12 @@ package wallet
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||
|
@ -26,31 +22,8 @@ import (
|
|||
// request a dry run of the txToOutputs call. It also makes sure a subsequent
|
||||
// non-dry run call produces a similar transaction to the dry-run.
|
||||
func TestTxToOutputsDryRun(t *testing.T) {
|
||||
// Set up a wallet.
|
||||
dir, err := ioutil.TempDir("", "createtx_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create db dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
seed, err := hdkeychain.GenerateSeed(hdkeychain.MinSeedBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create seed: %v", err)
|
||||
}
|
||||
|
||||
pubPass := []byte("hello")
|
||||
privPass := []byte("world")
|
||||
|
||||
loader := NewLoader(&chaincfg.TestNet3Params, dir, true, 250)
|
||||
w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create wallet: %v", err)
|
||||
}
|
||||
chainClient := &mockChainClient{}
|
||||
w.chainClient = chainClient
|
||||
if err := w.Unlock(privPass, time.After(10*time.Minute)); err != nil {
|
||||
t.Fatalf("unable to unlock wallet: %v", err)
|
||||
}
|
||||
w, cleanup := testWallet(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create an address we can use to send some coins to.
|
||||
addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0044)
|
||||
|
|
47
wallet/example_test.go
Normal file
47
wallet/example_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
)
|
||||
|
||||
// testWallet creates a test wallet and unlocks it.
|
||||
func testWallet(t *testing.T) (*Wallet, func()) {
|
||||
// Set up a wallet.
|
||||
dir, err := ioutil.TempDir("", "test_wallet")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create db dir: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Fatalf("could not cleanup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
seed, err := hdkeychain.GenerateSeed(hdkeychain.MinSeedBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create seed: %v", err)
|
||||
}
|
||||
|
||||
pubPass := []byte("hello")
|
||||
privPass := []byte("world")
|
||||
|
||||
loader := NewLoader(&chaincfg.TestNet3Params, dir, true, 250)
|
||||
w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create wallet: %v", err)
|
||||
}
|
||||
chainClient := &mockChainClient{}
|
||||
w.chainClient = chainClient
|
||||
if err := w.Unlock(privPass, time.After(10*time.Minute)); err != nil {
|
||||
t.Fatalf("unable to unlock wallet: %v", err)
|
||||
}
|
||||
|
||||
return w, cleanup
|
||||
}
|
|
@ -148,6 +148,7 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T
|
|||
MyOutputs: outputs,
|
||||
Fee: fee,
|
||||
Timestamp: details.Received.Unix(),
|
||||
Label: details.Label,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,6 +366,7 @@ type TransactionSummary struct {
|
|||
MyOutputs []TransactionSummaryOutput
|
||||
Fee btcutil.Amount
|
||||
Timestamp int64
|
||||
Label string
|
||||
}
|
||||
|
||||
// TransactionSummaryInput describes a transaction input that is relevant to the
|
||||
|
|
|
@ -65,6 +65,16 @@ var (
|
|||
// down.
|
||||
ErrWalletShuttingDown = errors.New("wallet shutting down")
|
||||
|
||||
// ErrUnknownTransaction is returned when an attempt is made to label
|
||||
// a transaction that is not known to the wallet.
|
||||
ErrUnknownTransaction = errors.New("cannot label transaction not " +
|
||||
"known to wallet")
|
||||
|
||||
// ErrTxLabelExists is returned when a transaction already has a label
|
||||
// and an attempt has been made to label it without setting overwrite
|
||||
// to true.
|
||||
ErrTxLabelExists = errors.New("transaction already labelled")
|
||||
|
||||
// Namespace bucket keys.
|
||||
waddrmgrNamespaceKey = []byte("waddrmgr")
|
||||
wtxmgrNamespaceKey = []byte("wtxmgr")
|
||||
|
@ -1591,6 +1601,58 @@ func (w *Wallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) {
|
|||
return pubKey, err
|
||||
}
|
||||
|
||||
// LabelTransaction adds a label to the transaction with the hash provided. The
|
||||
// call will fail if the label is too long, or if the transaction already has
|
||||
// a label and the overwrite boolean is not set.
|
||||
func (w *Wallet) LabelTransaction(hash chainhash.Hash, label string,
|
||||
overwrite bool) error {
|
||||
|
||||
// Check that the transaction is known to the wallet, and fail if it is
|
||||
// unknown. If the transaction is known, check whether it already has
|
||||
// a label.
|
||||
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
||||
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
||||
|
||||
dbTx, err := w.TxStore.TxDetails(txmgrNs, &hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the transaction looked up is nil, it was not found. We
|
||||
// do not allow labelling of unknown transactions so we fail.
|
||||
if dbTx == nil {
|
||||
return ErrUnknownTransaction
|
||||
}
|
||||
|
||||
_, err = wtxmgr.FetchTxLabel(txmgrNs, hash)
|
||||
return err
|
||||
})
|
||||
|
||||
switch err {
|
||||
// If no labels have been written yet, we can silence the error.
|
||||
// Likewise if there is no label, we do not need to do any overwrite
|
||||
// checks.
|
||||
case wtxmgr.ErrNoLabelBucket:
|
||||
case wtxmgr.ErrTxLabelNotFound:
|
||||
|
||||
// If we successfully looked up a label, fail if the overwrite param
|
||||
// is not set.
|
||||
case nil:
|
||||
if !overwrite {
|
||||
return ErrTxLabelExists
|
||||
}
|
||||
|
||||
// In another unrelated error occurred, return it.
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||
txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
||||
return w.TxStore.PutTxLabel(txmgrNs, hash, label)
|
||||
})
|
||||
}
|
||||
|
||||
// PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK
|
||||
// address.
|
||||
func (w *Wallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) {
|
||||
|
@ -3094,7 +3156,7 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
|
|||
// SendOutputs creates and sends payment transactions. It returns the
|
||||
// transaction upon success.
|
||||
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
|
||||
minconf int32, satPerKb btcutil.Amount) (*wire.MsgTx, error) {
|
||||
minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) {
|
||||
|
||||
// Ensure the outputs to be created adhere to the network's consensus
|
||||
// rules.
|
||||
|
@ -3118,7 +3180,7 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
txHash, err := w.reliablyPublishTransaction(createdTx.Tx)
|
||||
txHash, err := w.reliablyPublishTransaction(createdTx.Tx, label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -3311,8 +3373,8 @@ func (e *ErrReplacement) Unwrap() error {
|
|||
//
|
||||
// This function is unstable and will be removed once syncing code is moved out
|
||||
// of the wallet.
|
||||
func (w *Wallet) PublishTransaction(tx *wire.MsgTx) error {
|
||||
_, err := w.reliablyPublishTransaction(tx)
|
||||
func (w *Wallet) PublishTransaction(tx *wire.MsgTx, label string) error {
|
||||
_, err := w.reliablyPublishTransaction(tx, label)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -3321,7 +3383,9 @@ func (w *Wallet) PublishTransaction(tx *wire.MsgTx) error {
|
|||
// relevant database state, and finally possible removing the transaction from
|
||||
// the database (along with cleaning up all inputs used, and outputs created) if
|
||||
// the transaction is rejected by the backend.
|
||||
func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
|
||||
func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx,
|
||||
label string) (*chainhash.Hash, error) {
|
||||
|
||||
chainClient, err := w.requireChainClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -3336,7 +3400,19 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx) (*chainhash.Hash, er
|
|||
return nil, err
|
||||
}
|
||||
err = walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
||||
return w.addRelevantTx(dbTx, txRec, nil)
|
||||
if err := w.addRelevantTx(dbTx, txRec, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the tx label is empty, we can return early.
|
||||
if len(label) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there is a label we should write, get the namespace key
|
||||
// and record it in the tx store.
|
||||
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
||||
return w.TxStore.PutTxLabel(txmgrNs, tx.TxHash(), label)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
var (
|
||||
TstSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000")
|
||||
TstTx, _ = btcutil.NewTxFromBytes(TstSerializedTx)
|
||||
TstTxHash = TstTx.Hash()
|
||||
)
|
||||
|
||||
// TestLocateBirthdayBlock ensures we can properly map a block in the chain to a
|
||||
|
@ -83,3 +95,110 @@ func TestLocateBirthdayBlock(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLabelTransaction tests labelling of transactions with invalid labels,
|
||||
// and failure to label a transaction when it already has a label.
|
||||
func TestLabelTransaction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
// Whether the transaction should be known to the wallet.
|
||||
txKnown bool
|
||||
|
||||
// Whether the test should write an existing label to disk.
|
||||
existingLabel bool
|
||||
|
||||
// The overwrite parameter to call label transaction with.
|
||||
overwrite bool
|
||||
|
||||
// The error we expect to be returned.
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "existing label, not overwrite",
|
||||
txKnown: true,
|
||||
existingLabel: true,
|
||||
overwrite: false,
|
||||
expectedErr: ErrTxLabelExists,
|
||||
},
|
||||
{
|
||||
name: "existing label, overwritten",
|
||||
txKnown: true,
|
||||
existingLabel: true,
|
||||
overwrite: true,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "no prexisting label, ok",
|
||||
txKnown: true,
|
||||
existingLabel: false,
|
||||
overwrite: false,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "transaction unknown",
|
||||
txKnown: false,
|
||||
existingLabel: false,
|
||||
overwrite: false,
|
||||
expectedErr: ErrUnknownTransaction,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w, cleanup := testWallet(t)
|
||||
defer cleanup()
|
||||
|
||||
// If the transaction should be known to the store, we
|
||||
// write txdetail to disk.
|
||||
if test.txKnown {
|
||||
rec, err := wtxmgr.NewTxRecord(
|
||||
TstSerializedTx, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = walletdb.Update(w.db,
|
||||
func(tx walletdb.ReadWriteTx) error {
|
||||
|
||||
ns := tx.ReadWriteBucket(
|
||||
wtxmgrNamespaceKey,
|
||||
)
|
||||
|
||||
return w.TxStore.InsertTx(
|
||||
ns, rec, nil,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("could not insert tx: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If we want to setup an existing label for the purpose
|
||||
// of the test, write one to disk.
|
||||
if test.existingLabel {
|
||||
err := w.LabelTransaction(
|
||||
*TstTxHash, "existing label", false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write label: %v",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
newLabel := "new label"
|
||||
err := w.LabelTransaction(
|
||||
*TstTxHash, newLabel, test.overwrite,
|
||||
)
|
||||
if err != test.expectedErr {
|
||||
t.Fatalf("expected: %v, got: %v",
|
||||
test.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -39,6 +39,7 @@ type TxDetails struct {
|
|||
Block BlockMeta
|
||||
Credits []CreditRecord
|
||||
Debits []DebitRecord
|
||||
Label string
|
||||
}
|
||||
|
||||
// minedTxDetails fetches the TxDetails for the mined transaction with hash
|
||||
|
@ -90,7 +91,17 @@ func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, r
|
|||
|
||||
details.Debits = append(details.Debits, debIter.elem)
|
||||
}
|
||||
return &details, debIter.err
|
||||
if debIter.err != nil {
|
||||
return nil, debIter.err
|
||||
}
|
||||
|
||||
// Finally, we add the transaction label to details.
|
||||
details.Label, err = s.TxLabel(ns, *txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &details, nil
|
||||
}
|
||||
|
||||
// unminedTxDetails fetches the TxDetails for the unmined transaction with the
|
||||
|
@ -158,9 +169,40 @@ func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash,
|
|||
})
|
||||
}
|
||||
|
||||
// Finally, we add the transaction label to details.
|
||||
details.Label, err = s.TxLabel(ns, *txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &details, nil
|
||||
}
|
||||
|
||||
// TxLabel looks up a transaction label for the txHash provided. If the store
|
||||
// has no labels in it, or the specific txHash does not have a label, an empty
|
||||
// string and no error are returned.
|
||||
func (s *Store) TxLabel(ns walletdb.ReadBucket, txHash chainhash.Hash) (string,
|
||||
error) {
|
||||
|
||||
label, err := FetchTxLabel(ns, txHash)
|
||||
switch err {
|
||||
// If there are no saved labels yet (the bucket has not been created) or
|
||||
// there is not a label for this particular tx, we ignore the error.
|
||||
case ErrNoLabelBucket:
|
||||
fallthrough
|
||||
case ErrTxLabelNotFound:
|
||||
return "", nil
|
||||
|
||||
// If we found the label, we return it.
|
||||
case nil:
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// Otherwise, another error occurred while looking uo the label, so we
|
||||
// return it.
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TxDetails looks up all recorded details regarding a transaction with some
|
||||
// hash. In case of a hash collision, the most recent transaction with a
|
||||
// matching hash is returned.
|
||||
|
@ -296,52 +338,13 @@ func (s *Store) rangeBlockTransactions(ns walletdb.ReadBucket, begin, end int32,
|
|||
"block %v", txHash, block.Height)
|
||||
return false, storeError(ErrData, str, nil)
|
||||
}
|
||||
detail := TxDetails{
|
||||
Block: BlockMeta{
|
||||
Block: block.Block,
|
||||
Time: block.Time,
|
||||
},
|
||||
}
|
||||
err := readRawTxRecord(&txHash, v, &detail.TxRecord)
|
||||
|
||||
detail, err := s.minedTxDetails(ns, &txHash, k, v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
credIter := makeReadCreditIterator(ns, k)
|
||||
for credIter.next() {
|
||||
if int(credIter.elem.Index) >= len(detail.MsgTx.TxOut) {
|
||||
str := "saved credit index exceeds number of outputs"
|
||||
return false, storeError(ErrData, str, nil)
|
||||
}
|
||||
|
||||
// The credit iterator does not record whether
|
||||
// this credit was spent by an unmined
|
||||
// transaction, so check that here.
|
||||
if !credIter.elem.Spent {
|
||||
k := canonicalOutPoint(&txHash, credIter.elem.Index)
|
||||
spent := existsRawUnminedInput(ns, k) != nil
|
||||
credIter.elem.Spent = spent
|
||||
}
|
||||
detail.Credits = append(detail.Credits, credIter.elem)
|
||||
}
|
||||
if credIter.err != nil {
|
||||
return false, credIter.err
|
||||
}
|
||||
|
||||
debIter := makeReadDebitIterator(ns, k)
|
||||
for debIter.next() {
|
||||
if int(debIter.elem.Index) >= len(detail.MsgTx.TxIn) {
|
||||
str := "saved debit index exceeds number of inputs"
|
||||
return false, storeError(ErrData, str, nil)
|
||||
}
|
||||
|
||||
detail.Debits = append(detail.Debits, debIter.elem)
|
||||
}
|
||||
if debIter.err != nil {
|
||||
return false, debIter.err
|
||||
}
|
||||
|
||||
details = append(details, detail)
|
||||
details = append(details, *detail)
|
||||
}
|
||||
|
||||
// Every block record must have at least one transaction, so it
|
||||
|
|
91
wtxmgr/tx.go
91
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue