decred.org/dcrdex@v1.0.5/client/asset/btc/txdb.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package btc
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/binary"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"sync"
    15  	"sync/atomic"
    16  	"time"
    17  
    18  	"decred.org/dcrdex/client/asset"
    19  	"decred.org/dcrdex/dex"
    20  	"github.com/dgraph-io/badger"
    21  )
    22  
    23  type ExtendedWalletTx struct {
    24  	*asset.WalletTransaction
    25  	// Create bond transactions are added to the store before
    26  	// they are submitted.
    27  	Submitted bool `json:"submitted"`
    28  }
    29  
    30  // "b" and "c" must be the first two prefixes.
    31  // getPendingTxs relies on this.
    32  var blockPrefix = []byte("b")
    33  var pendingPrefix = []byte("c")
    34  var lastQueryKey = []byte("lq")
    35  var txPrefix = []byte("t")
    36  var maxPendingKey = pendingKey(math.MaxUint64)
    37  
    38  // pendingKey maps an index to an extendedWalletTransaction. The index is
    39  // required as there may be multiple pending transactions at the same time.
    40  func pendingKey(i uint64) []byte {
    41  	key := make([]byte, len(pendingPrefix)+8)
    42  	copy(key, pendingPrefix)
    43  	binary.BigEndian.PutUint64(key[len(pendingPrefix):], i)
    44  	return key
    45  }
    46  
    47  // blockKey maps a block height and an index to an extendedWalletTransaction.
    48  // The index is required as there may be multiple transactions in the same
    49  // block.
    50  func blockKey(blockHeight, index uint64) []byte {
    51  	key := make([]byte, len(blockPrefix)+16)
    52  	copy(key, blockPrefix)
    53  	binary.BigEndian.PutUint64(key[len(blockPrefix):], blockHeight)
    54  	binary.BigEndian.PutUint64(key[len(blockPrefix)+8:], index)
    55  	return key
    56  }
    57  
    58  func parseBlockKey(key []byte) (blockHeight, index uint64) {
    59  	blockHeight = binary.BigEndian.Uint64(key[len(blockPrefix):])
    60  	index = binary.BigEndian.Uint64(key[len(blockPrefix)+8:])
    61  	return
    62  }
    63  
    64  // txKey maps a txid to a blockKey or pendingKey.
    65  func txKey(txid string) []byte {
    66  	key := make([]byte, len(txPrefix)+len([]byte(txid)))
    67  	copy(key, txPrefix)
    68  	copy(key[len(txPrefix):], []byte(txid))
    69  	return key
    70  }
    71  
    72  type BadgerTxDB struct {
    73  	*badger.DB
    74  	filePath string
    75  	log      dex.Logger
    76  	seq      *badger.Sequence
    77  	running  atomic.Bool
    78  	wg       sync.WaitGroup
    79  	ctx      context.Context
    80  }
    81  
    82  // badgerLoggerWrapper wraps dex.Logger and translates Warnf to Warningf to
    83  // satisfy badger.Logger. It also lowers the log level of Infof to Debugf
    84  // and Debugf to Tracef.
    85  type badgerLoggerWrapper struct {
    86  	dex.Logger
    87  }
    88  
    89  var _ badger.Logger = (*badgerLoggerWrapper)(nil)
    90  
    91  // Debugf -> dex.Logger.Tracef
    92  func (log *badgerLoggerWrapper) Debugf(s string, a ...interface{}) {
    93  	log.Tracef(s, a...)
    94  }
    95  
    96  // Infof -> dex.Logger.Debugf
    97  func (log *badgerLoggerWrapper) Infof(s string, a ...interface{}) {
    98  	log.Debugf(s, a...)
    99  }
   100  
   101  // Warningf -> dex.Logger.Warnf
   102  func (log *badgerLoggerWrapper) Warningf(s string, a ...interface{}) {
   103  	log.Warnf(s, a...)
   104  }
   105  
   106  func NewBadgerTxDB(filePath string, log dex.Logger) *BadgerTxDB {
   107  	return &BadgerTxDB{
   108  		filePath: filePath,
   109  		log:      log,
   110  	}
   111  }
   112  
   113  func (db *BadgerTxDB) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   114  	// If memory use is a concern, could try
   115  	//   .WithValueLogLoadingMode(options.FileIO) // default options.MemoryMap
   116  	//   .WithMaxTableSize(sz int64); // bytes, default 6MB
   117  	//   .WithValueLogFileSize(sz int64), bytes, default 1 GB, must be 1MB <= sz <= 1GB
   118  	opts := badger.DefaultOptions(db.filePath).WithLogger(&badgerLoggerWrapper{db.log})
   119  	var err error
   120  	db.DB, err = badger.Open(opts)
   121  	if err == badger.ErrTruncateNeeded {
   122  		// Probably a Windows thing.
   123  		// https://github.com/dgraph-io/badger/issues/744
   124  		db.log.Warnf("newTxHistoryStore badger db: %v", err)
   125  		// Try again with value log truncation enabled.
   126  		opts.Truncate = true
   127  		db.log.Warnf("Attempting to reopen badger DB with the Truncate option set...")
   128  		db.DB, err = badger.Open(opts)
   129  	}
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	db.ctx = ctx
   134  	db.seq, err = db.GetSequence([]byte("seq"), 10)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	db.running.Store(true)
   140  
   141  	var wg sync.WaitGroup
   142  
   143  	wg.Add(1)
   144  	go func() {
   145  		defer wg.Done()
   146  
   147  		ticker := time.NewTicker(5 * time.Minute)
   148  		defer ticker.Stop()
   149  		for {
   150  			select {
   151  			case <-ticker.C:
   152  				err := db.RunValueLogGC(0.5)
   153  				if err != nil && !errors.Is(err, badger.ErrNoRewrite) {
   154  					db.log.Errorf("garbage collection error: %v", err)
   155  				}
   156  			case <-ctx.Done():
   157  				db.running.Store(false)
   158  				db.wg.Wait()
   159  				if err := db.seq.Release(); err != nil {
   160  					db.log.Errorf("error releasing sequence: %v", err)
   161  				}
   162  				db.Close()
   163  				return
   164  			}
   165  		}
   166  	}()
   167  
   168  	return &wg, nil
   169  }
   170  
   171  // badgerDB returns ErrConflict when a read happening in a update (read/write)
   172  // transaction is stale. This function retries updates multiple times in
   173  // case of conflicts.
   174  func (db *BadgerTxDB) handleConflictWithBackoff(update func() error) (err error) {
   175  	maxRetries := 10
   176  	sleepTime := 5 * time.Millisecond
   177  
   178  	for i := 0; i < maxRetries; i++ {
   179  		sleepTime *= 2
   180  		err = update()
   181  		if err != badger.ErrConflict {
   182  			return err
   183  		}
   184  		time.Sleep(sleepTime)
   185  	}
   186  
   187  	return err
   188  }
   189  
   190  func (db *BadgerTxDB) newBlockKey(blockNumber uint64) ([]byte, error) {
   191  	seq, err := db.seq.Next()
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	if blockNumber == 0 {
   196  		return pendingKey(seq), nil
   197  	}
   198  	return blockKey(blockNumber, seq), nil
   199  }
   200  
   201  func hasPrefix(b, prefix []byte) bool {
   202  	if len(b) < len(prefix) {
   203  		return false
   204  	}
   205  	return bytes.Equal(b[:len(prefix)], prefix)
   206  }
   207  
   208  func (db *BadgerTxDB) storeTx(tx *ExtendedWalletTx) error {
   209  	return db.Update(func(txn *badger.Txn) error {
   210  		txKey := txKey(tx.ID)
   211  		txKeyItem, err := txn.Get(txKey)
   212  		if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
   213  			return err
   214  		}
   215  
   216  		var key []byte
   217  		if err == nil { // already stored
   218  			currBlockKey, err := txKeyItem.ValueCopy(nil)
   219  			if err != nil {
   220  				return err
   221  			}
   222  			err = txn.Delete(currBlockKey)
   223  			if err != nil {
   224  				return err
   225  			}
   226  			// Keep the same key unless a pending tx that has been confirmed,
   227  			// or if the block number has changed indicating a reorg.
   228  			if hasPrefix(currBlockKey, pendingPrefix) && tx.BlockNumber == 0 {
   229  				key = currBlockKey
   230  			} else if hasPrefix(currBlockKey, blockPrefix) {
   231  				blockHeight, _ := parseBlockKey(currBlockKey)
   232  				if blockHeight == tx.BlockNumber {
   233  					key = currBlockKey
   234  				}
   235  			}
   236  		}
   237  
   238  		if key == nil {
   239  			key, err = db.newBlockKey(tx.BlockNumber)
   240  			if err != nil {
   241  				return err
   242  			}
   243  		}
   244  
   245  		txB, err := json.Marshal(tx)
   246  		if err != nil {
   247  			return err
   248  		}
   249  
   250  		err = txn.Set(txKey, key)
   251  		if err != nil {
   252  			return err
   253  		}
   254  
   255  		return txn.Set(key, txB)
   256  	})
   257  }
   258  
   259  // StoreTx stores a transaction in the database.
   260  func (db *BadgerTxDB) StoreTx(tx *ExtendedWalletTx) error {
   261  	db.wg.Add(1)
   262  	defer db.wg.Done()
   263  	if !db.running.Load() {
   264  		return fmt.Errorf("database is not running")
   265  	}
   266  
   267  	return db.handleConflictWithBackoff(func() error { return db.storeTx(tx) })
   268  }
   269  
   270  func (db *BadgerTxDB) markTxAsSubmitted(txID string) error {
   271  	return db.Update(func(txn *badger.Txn) error {
   272  		txKey := txKey(txID)
   273  		txKeyItem, err := txn.Get(txKey)
   274  		if err != nil {
   275  			return asset.CoinNotFoundError
   276  		}
   277  
   278  		blockKey, err := txKeyItem.ValueCopy(nil)
   279  		if err != nil {
   280  			return err
   281  		}
   282  
   283  		blockItem, err := txn.Get(blockKey)
   284  		if err != nil {
   285  			return err
   286  		}
   287  
   288  		wtB, err := blockItem.ValueCopy(nil)
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		var wt ExtendedWalletTx
   294  		if err := json.Unmarshal(wtB, &wt); err != nil {
   295  			return err
   296  		}
   297  
   298  		wt.Submitted = true
   299  		submittedWt, err := json.Marshal(wt)
   300  		if err != nil {
   301  			return err
   302  		}
   303  
   304  		return txn.Set(blockKey, submittedWt)
   305  	})
   306  }
   307  
   308  // MarkTxAsSubmitted should be called when a previously stored transaction
   309  // that had not yet been sent to the network is sent to the network.
   310  // asset.CoinNotFoundError is returned if the transaction is not in the
   311  // database.
   312  func (db *BadgerTxDB) MarkTxAsSubmitted(txID string) error {
   313  	db.wg.Add(1)
   314  	defer db.wg.Done()
   315  	if !db.running.Load() {
   316  		return fmt.Errorf("database is not running")
   317  	}
   318  
   319  	return db.handleConflictWithBackoff(func() error { return db.markTxAsSubmitted(txID) })
   320  }
   321  
   322  // GetTxs retrieves n transactions from the database. refID optionally
   323  // takes a transaction ID, and returns that transaction and the at most
   324  // (n - 1) transactions that were made either before or after it, depending
   325  // on the value of past. If refID is nil, the most recent n transactions
   326  // are returned, and the value of past is ignored. If the transaction with
   327  // ID refID is not in the database, asset.CoinNotFoundError is returned.
   328  // Unsubmitted transactions are not returned.
   329  func (db *BadgerTxDB) GetTxs(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
   330  	db.wg.Add(1)
   331  	defer db.wg.Done()
   332  	if !db.running.Load() {
   333  		return nil, fmt.Errorf("database is not running")
   334  	}
   335  
   336  	var txs []*asset.WalletTransaction
   337  	err := db.View(func(txn *badger.Txn) error {
   338  		var startKey []byte
   339  		if refID != nil {
   340  			txKey := txKey(*refID)
   341  			txKeyItem, err := txn.Get(txKey)
   342  			if err != nil {
   343  				return asset.CoinNotFoundError
   344  			}
   345  
   346  			startKey, err = txKeyItem.ValueCopy(nil)
   347  			if err != nil {
   348  				return err
   349  			}
   350  		}
   351  		if startKey == nil {
   352  			past = true
   353  			startKey = maxPendingKey
   354  		}
   355  
   356  		opts := badger.DefaultIteratorOptions
   357  		opts.Reverse = past
   358  		it := txn.NewIterator(opts)
   359  		defer it.Close()
   360  
   361  		canIterate := func() bool {
   362  			validPrefix := it.ValidForPrefix(blockPrefix) || it.ValidForPrefix(pendingPrefix)
   363  			withinLimit := n <= 0 || len(txs) < n
   364  			return validPrefix && withinLimit
   365  		}
   366  		for it.Seek(startKey); canIterate(); it.Next() {
   367  			item := it.Item()
   368  			wtB, err := item.ValueCopy(nil)
   369  			if err != nil {
   370  				return err
   371  			}
   372  			var wt ExtendedWalletTx
   373  			if err := json.Unmarshal(wtB, &wt); err != nil {
   374  				return err
   375  			}
   376  			if !wt.Submitted {
   377  				continue
   378  			}
   379  			if past {
   380  				txs = append(txs, wt.WalletTransaction)
   381  			} else {
   382  				txs = append([]*asset.WalletTransaction{wt.WalletTransaction}, txs...)
   383  			}
   384  		}
   385  
   386  		return nil
   387  	})
   388  	return txs, err
   389  }
   390  
   391  // GetTx retrieves a transaction by its ID. If the transaction is not in
   392  // the database, asset.CoinNotFoundError is returned.
   393  func (db *BadgerTxDB) GetTx(txID string) (*asset.WalletTransaction, error) {
   394  	db.wg.Add(1)
   395  	defer db.wg.Done()
   396  	if !db.running.Load() {
   397  		return nil, fmt.Errorf("database is not running")
   398  	}
   399  
   400  	txs, err := db.GetTxs(1, &txID, false)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	if len(txs) == 0 {
   405  		// This should never happen.
   406  		return nil, fmt.Errorf("no results returned from getTxs")
   407  	}
   408  	return txs[0], nil
   409  }
   410  
   411  // GetPendingTxs returns all transactions that have not yet been confirmed.
   412  func (db *BadgerTxDB) GetPendingTxs() ([]*ExtendedWalletTx, error) {
   413  	db.wg.Add(1)
   414  	defer db.wg.Done()
   415  	if !db.running.Load() {
   416  		return nil, fmt.Errorf("database is not running")
   417  	}
   418  
   419  	var txs []*ExtendedWalletTx
   420  	err := db.View(func(txn *badger.Txn) error {
   421  		opts := badger.DefaultIteratorOptions
   422  		opts.Reverse = true
   423  		it := txn.NewIterator(opts)
   424  		defer it.Close()
   425  
   426  		for it.Seek(maxPendingKey); it.Valid(); it.Next() {
   427  			item := it.Item()
   428  			wtB, err := item.ValueCopy(nil)
   429  			if err != nil {
   430  				return err
   431  			}
   432  			var wt ExtendedWalletTx
   433  			if err := json.Unmarshal(wtB, &wt); err != nil {
   434  				return err
   435  			}
   436  
   437  			if !wt.Confirmed {
   438  				txs = append(txs, &wt)
   439  			}
   440  		}
   441  
   442  		return nil
   443  	})
   444  
   445  	return txs, err
   446  }
   447  
   448  func (db *BadgerTxDB) removeTx(txID string) error {
   449  	return db.Update(func(txn *badger.Txn) error {
   450  		txKey := txKey(txID)
   451  		txKeyItem, err := txn.Get(txKey)
   452  		if err != nil {
   453  			return asset.CoinNotFoundError
   454  		}
   455  
   456  		blockKey, err := txKeyItem.ValueCopy(nil)
   457  		if err != nil {
   458  			return err
   459  		}
   460  
   461  		if err := txn.Delete(txKey); err != nil {
   462  			return err
   463  		}
   464  
   465  		return txn.Delete(blockKey)
   466  	})
   467  }
   468  
   469  // RemoveTx removes a transaction from the database. If the transaction is
   470  // not in the database, asset.CoinNotFoundError is returned.
   471  func (db *BadgerTxDB) RemoveTx(txID string) error {
   472  	db.wg.Add(1)
   473  	defer db.wg.Done()
   474  	if !db.running.Load() {
   475  		return fmt.Errorf("database is not running")
   476  	}
   477  
   478  	return db.handleConflictWithBackoff(func() error { return db.removeTx(txID) })
   479  }
   480  
   481  func (db *BadgerTxDB) setLastReceiveTxQuery(block uint64) error {
   482  	return db.Update(func(txn *badger.Txn) error {
   483  		// use binary big endian
   484  		b := make([]byte, 8)
   485  		binary.BigEndian.PutUint64(b, block)
   486  		return txn.Set(lastQueryKey, b)
   487  	})
   488  }
   489  
   490  // SetLastReceiveTxQuery stores the last time the wallet was queried for
   491  // receive transactions. This is required to know how far back to query
   492  // for incoming transactions that were received while the wallet is
   493  // offline.
   494  func (db *BadgerTxDB) SetLastReceiveTxQuery(block uint64) error {
   495  	db.wg.Add(1)
   496  	defer db.wg.Done()
   497  	if !db.running.Load() {
   498  		return fmt.Errorf("database is not running")
   499  	}
   500  
   501  	return db.handleConflictWithBackoff(func() error { return db.setLastReceiveTxQuery(block) })
   502  }
   503  
   504  const ErrNeverQueried = dex.ErrorKind("never queried")
   505  
   506  // GetLastReceiveTxQuery retrieves the last time the wallet was queried for
   507  // receive transactions.
   508  func (db *BadgerTxDB) GetLastReceiveTxQuery() (uint64, error) {
   509  	db.wg.Add(1)
   510  	defer db.wg.Done()
   511  	if !db.running.Load() {
   512  		return 0, fmt.Errorf("database is not running")
   513  	}
   514  
   515  	var block uint64
   516  	err := db.View(func(txn *badger.Txn) error {
   517  		item, err := txn.Get(lastQueryKey)
   518  		if errors.Is(err, badger.ErrKeyNotFound) {
   519  			return ErrNeverQueried
   520  		}
   521  		if err != nil {
   522  			return err
   523  		}
   524  		b, err := item.ValueCopy(nil)
   525  		if err != nil {
   526  			return err
   527  		}
   528  		block = binary.BigEndian.Uint64(b)
   529  		return nil
   530  	})
   531  	return block, err
   532  }