decred.org/dcrdex@v1.0.5/client/asset/eth/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 eth
     5  
     6  import (
     7  	"context"
     8  	"encoding/binary"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"math/big"
    14  	"sync"
    15  	"time"
    16  
    17  	"decred.org/dcrdex/client/asset"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/utils"
    20  	"github.com/dgraph-io/badger"
    21  	"github.com/ethereum/go-ethereum/common"
    22  	"github.com/ethereum/go-ethereum/core/types"
    23  )
    24  
    25  // extendedWalletTx is an asset.WalletTransaction extended with additional
    26  // fields used for tracking transactions.
    27  type extendedWalletTx struct {
    28  	*asset.WalletTransaction
    29  	BlockSubmitted uint64         `json:"blockSubmitted"`
    30  	SubmissionTime uint64         `json:"timeStamp"` // seconds
    31  	Nonce          *big.Int       `json:"nonce"`
    32  	Receipt        *types.Receipt `json:"receipt,omitempty"`
    33  	RawTx          dex.Bytes      `json:"rawTx"`
    34  	// NonceReplacement is a transaction with the same nonce that was accepted
    35  	// by the network, meaning this tx was not applied.
    36  	NonceReplacement string `json:"nonceReplacement,omitempty"`
    37  	// FeeReplacement is true if the NonceReplacement is the same tx as this
    38  	// one, just with higher fees.
    39  	FeeReplacement bool `json:"feeReplacement,omitempty"`
    40  	// AssumedLost will be set to true if a transaction is assumed to be lost.
    41  	// This typically requires feedback from the user in response to an
    42  	// ActionRequiredNote.
    43  	AssumedLost bool `json:"assumedLost,omitempty"`
    44  
    45  	txHash          common.Hash
    46  	lastCheck       uint64
    47  	savedToDB       bool
    48  	lastBroadcast   time.Time
    49  	lastFeeCheck    time.Time
    50  	actionRequested bool
    51  	actionIgnored   time.Time
    52  	indexed         bool
    53  }
    54  
    55  func (t *extendedWalletTx) age() time.Duration {
    56  	return time.Since(time.Unix(int64(t.SubmissionTime), 0))
    57  }
    58  
    59  func (t *extendedWalletTx) tx() (*types.Transaction, error) {
    60  	tx := new(types.Transaction)
    61  	return tx, tx.UnmarshalBinary(t.RawTx)
    62  }
    63  
    64  var (
    65  	// noncePrefix is the prefix for the key used to map a nonce to an
    66  	// extendedWalletTx.
    67  	noncePrefix = []byte("nonce-")
    68  	// txHashPrefix is the prefix for the key used to map a transaction hash
    69  	// to a nonce key.
    70  	txHashPrefix = []byte("txHash-")
    71  	// dbVersionKey is the key used to store the database version.
    72  	dbVersionKey = []byte("dbVersion")
    73  )
    74  
    75  func nonceKey(nonce uint64) []byte {
    76  	key := make([]byte, len(noncePrefix)+8)
    77  	copy(key, noncePrefix)
    78  	binary.BigEndian.PutUint64(key[len(noncePrefix):], nonce)
    79  	return key
    80  }
    81  
    82  func txKey(txHash common.Hash) []byte {
    83  	key := make([]byte, len(txHashPrefix)+20)
    84  	copy(key, txHashPrefix)
    85  	copy(key[len(txHashPrefix):], txHash[:])
    86  	return key
    87  }
    88  
    89  // badgerDB returns ErrConflict when a read happening in a update (read/write)
    90  // transaction is stale. This function retries updates multiple times in
    91  // case of conflicts.
    92  func (db *badgerTxDB) Update(f func(txn *badger.Txn) error) (err error) {
    93  	db.updateWG.Add(1)
    94  	defer db.updateWG.Done()
    95  
    96  	const maxRetries = 10
    97  	sleepTime := 5 * time.Millisecond
    98  
    99  	for i := 0; i < maxRetries; i++ {
   100  		if err = db.DB.Update(f); err == nil || !errors.Is(err, badger.ErrConflict) {
   101  			return err
   102  		}
   103  		sleepTime *= 2
   104  		time.Sleep(sleepTime)
   105  	}
   106  
   107  	return err
   108  }
   109  
   110  var maxNonceKey = nonceKey(math.MaxUint64)
   111  
   112  // initialDBVersion only contained mappings from txHash -> monitoredTx.
   113  // const initialDBVersion = 0
   114  
   115  // prefixDBVersion contains two mappings each marked with a prefix:
   116  //
   117  //	nonceKey -> extendedWalletTx (noncePrefix)
   118  //	txHash -> nonceKey (txHashPrefix)
   119  // const prefixDBVersion = 1
   120  
   121  // txMappingVersion reverses the semantics so that all txs are accessible
   122  // by txHash.
   123  //
   124  // nonceKey -> best-known txHash
   125  // txHash -> extendedWalletTx, which contains a nonce
   126  const txMappingVersion = 2
   127  
   128  const txDBVersion = txMappingVersion
   129  
   130  type txDB interface {
   131  	dex.Connector
   132  	storeTx(wt *extendedWalletTx) error
   133  	getTxs(n int, refID *common.Hash, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error)
   134  	// getTx gets a single transaction. It is not an error if the tx is not known.
   135  	// In that case, a nil tx is returned.
   136  	getTx(txHash common.Hash) (*extendedWalletTx, error)
   137  	// getPendingTxs returns any recent txs that are not confirmed, ordered
   138  	// by nonce lowest-first.
   139  	getPendingTxs() ([]*extendedWalletTx, error)
   140  }
   141  
   142  type badgerTxDB struct {
   143  	*badger.DB
   144  	filePath string
   145  	log      dex.Logger
   146  	updateWG sync.WaitGroup
   147  }
   148  
   149  var _ txDB = (*badgerTxDB)(nil)
   150  
   151  func newBadgerTxDB(filePath string, log dex.Logger) (*badgerTxDB, error) {
   152  	// If memory use is a concern, could try
   153  	//   .WithValueLogLoadingMode(options.FileIO) // default options.MemoryMap
   154  	//   .WithMaxTableSize(sz int64); // bytes, default 6MB
   155  	//   .WithValueLogFileSize(sz int64), bytes, default 1 GB, must be 1MB <= sz <= 1GB
   156  	opts := badger.DefaultOptions(filePath).WithLogger(&badgerLoggerWrapper{log})
   157  	var err error
   158  	bdb, err := badger.Open(opts)
   159  	if err == badger.ErrTruncateNeeded {
   160  		// Probably a Windows thing.
   161  		// https://github.com/dgraph-io/badger/issues/744
   162  		log.Warnf("error opening badger db: %v", err)
   163  		// Try again with value log truncation enabled.
   164  		opts.Truncate = true
   165  		log.Warnf("Attempting to reopen badger DB with the Truncate option set...")
   166  		bdb, err = badger.Open(opts)
   167  	}
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	db := &badgerTxDB{
   173  		DB:       bdb,
   174  		filePath: filePath,
   175  		log:      log,
   176  	}
   177  	return db, nil
   178  }
   179  
   180  func (db *badgerTxDB) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   181  	if err := db.updateVersion(); err != nil {
   182  		return nil, fmt.Errorf("failed to update db: %w", err)
   183  	}
   184  
   185  	var wg sync.WaitGroup
   186  
   187  	wg.Add(1)
   188  	go func() {
   189  		defer wg.Done()
   190  		defer db.Close()
   191  		defer db.updateWG.Wait()
   192  		ticker := time.NewTicker(5 * time.Minute)
   193  		defer ticker.Stop()
   194  		for {
   195  			select {
   196  			case <-ticker.C:
   197  				err := db.RunValueLogGC(0.5)
   198  				if err != nil && !errors.Is(err, badger.ErrNoRewrite) {
   199  					db.log.Errorf("garbage collection error: %v", err)
   200  				}
   201  			case <-ctx.Done():
   202  				return
   203  			}
   204  		}
   205  	}()
   206  	return &wg, nil
   207  }
   208  
   209  // txForNonce gets the registered for the given nonce.
   210  func txForNonce(txn *badger.Txn, nonce uint64) (tx *extendedWalletTx, err error) {
   211  	nk := nonceKey(nonce)
   212  	txHashi, err := txn.Get(nk)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  	return tx, txHashi.Value(func(txHashB []byte) error {
   217  		var txHash common.Hash
   218  		copy(txHash[:], txHashB)
   219  		txi, err := txn.Get(txKey(txHash))
   220  		if err != nil {
   221  			return err
   222  		}
   223  		return txi.Value(func(wtB []byte) error {
   224  			tx, err = unmarshalTx(wtB)
   225  			return err
   226  		})
   227  	})
   228  }
   229  
   230  // txForHash get the extendedWalletTx at the given tx hash and checks for any
   231  // unsaved nonce replacement.
   232  func txForHash(txn *badger.Txn, txHash common.Hash) (wt *extendedWalletTx, err error) {
   233  	txi, err := txn.Get(txKey(txHash))
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	return wt, txi.Value(func(wtB []byte) error {
   238  		wt, err = unmarshalTx(wtB)
   239  		if err != nil || wt.Confirmed || wt.NonceReplacement != "" {
   240  			return err
   241  		}
   242  		nonceTx, err := txForNonce(txn, wt.Nonce.Uint64())
   243  		if err != nil {
   244  			return err
   245  		}
   246  		if nonceTx.txHash != wt.txHash && nonceTx.Confirmed {
   247  			wt.NonceReplacement = wt.txHash.String()
   248  		}
   249  		return nil
   250  	})
   251  }
   252  
   253  // updateVersion updates the DB to the latest version. In version 0,
   254  // only a mapping from txHash to monitoredTx was stored, with no
   255  // prefixes.
   256  func (db *badgerTxDB) updateVersion() error {
   257  	// Check if the database version is stored. If not, the db
   258  	// is version 0.
   259  	var version int
   260  	err := db.View(func(txn *badger.Txn) error {
   261  		item, err := txn.Get(dbVersionKey)
   262  		if err != nil {
   263  			if errors.Is(err, badger.ErrKeyNotFound) {
   264  				return nil
   265  			}
   266  			return err
   267  		}
   268  		return item.Value(func(versionB []byte) error {
   269  			version = int(binary.BigEndian.Uint64(versionB))
   270  			return nil
   271  		})
   272  	})
   273  	if err != nil {
   274  		db.log.Errorf("error retrieving database version: %v", err)
   275  	}
   276  
   277  	if version < txMappingVersion {
   278  		if err := db.DB.DropAll(); err != nil {
   279  			return fmt.Errorf("error deleting DB entries for version upgrade: %w", err)
   280  		}
   281  		versionB := make([]byte, 8)
   282  		binary.BigEndian.PutUint64(versionB, txMappingVersion)
   283  		if err = db.Update(func(txn *badger.Txn) error {
   284  			return txn.Set(dbVersionKey, versionB)
   285  		}); err != nil {
   286  			return err
   287  		}
   288  		db.log.Infof("Upgraded DB to version %d by deleting everything and starting from scratch.", txMappingVersion)
   289  	} else if version > txDBVersion {
   290  		return fmt.Errorf("database version %d is not supported", version)
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  // storeTx stores a mapping from nonce to extendedWalletTx and a mapping from
   297  // transaction hash to nonce so transactions can be looked up by hash. If a
   298  // nonce already exists, the extendedWalletTx is overwritten.
   299  func (db *badgerTxDB) storeTx(wt *extendedWalletTx) error {
   300  	wtB, err := json.Marshal(wt)
   301  	if err != nil {
   302  		return err
   303  	}
   304  	nonce := wt.Nonce.Uint64()
   305  
   306  	return db.Update(func(txn *badger.Txn) error {
   307  		// If there is not a confirmed tx at this tx's nonce, map the nonce
   308  		// to this tx.
   309  		nonceTx, err := txForNonce(txn, nonce)
   310  		if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
   311  			return fmt.Errorf("error reading nonce tx: %w", err)
   312  		}
   313  		// If we don't have a tx stored at the nonce or the tx stored at the
   314  		// nonce is not confirmed, put this one there instead, unless this one
   315  		// has been marked as nonce-replaced.
   316  		if (nonceTx == nil || !nonceTx.Confirmed) && wt.NonceReplacement == "" {
   317  			if err := txn.Set(nonceKey(nonce), wt.txHash[:]); err != nil {
   318  				return fmt.Errorf("error mapping nonce to tx hash: %w", err)
   319  			}
   320  		}
   321  		// Store the tx at its hash.
   322  		return txn.Set(txKey(wt.txHash), wtB)
   323  	})
   324  }
   325  
   326  // getTx gets a single transaction. It is not an error if the tx is not known.
   327  // In that case, a nil tx is returned.
   328  func (db *badgerTxDB) getTx(txHash common.Hash) (tx *extendedWalletTx, err error) {
   329  	return tx, db.View(func(txn *badger.Txn) error {
   330  		tx, err = txForHash(txn, txHash)
   331  		if errors.Is(err, badger.ErrKeyNotFound) {
   332  			return nil
   333  		}
   334  		return err
   335  	})
   336  }
   337  
   338  // unmarshalTx attempts to decode the binary tx and sets some unexported fields.
   339  func unmarshalTx(wtB []byte) (wt *extendedWalletTx, err error) {
   340  	if err = json.Unmarshal(wtB, &wt); err != nil {
   341  		return nil, err
   342  	}
   343  	wt.txHash = common.HexToHash(wt.ID)
   344  	wt.lastBroadcast = time.Unix(int64(wt.SubmissionTime), 0)
   345  	wt.savedToDB = true
   346  	return
   347  }
   348  
   349  // getTxs fetches n transactions. If no refID is provided, getTxs returns the
   350  // n most recent txs in reverse-nonce order. If no refID is provided, the past
   351  // argument is ignored. If a refID is provided, getTxs will return n txs
   352  // starting with the nonce of the tx referenced. When refID is provided, and
   353  // past is false, the results will be in increasing order starting at and
   354  // including the nonce of the referenced tx. If refID is provided and past
   355  // is true, the results will be in decreasing nonce order starting at and
   356  // including the referenced tx. No orphans will be included in the results.
   357  // If a non-nil refID is not found, asset.CoinNotFoundError is returned.
   358  func (db *badgerTxDB) getTxs(n int, refID *common.Hash, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error) {
   359  	txs := make([]*asset.WalletTransaction, 0, n)
   360  
   361  	return txs, db.View(func(txn *badger.Txn) error {
   362  		opts := badger.DefaultIteratorOptions
   363  		opts.Reverse = true // If non refID, it's always reverse
   364  		opts.Prefix = noncePrefix
   365  		startNonceKey := maxNonceKey
   366  		if refID != nil {
   367  			opts.Reverse = past
   368  			// Get the nonce for the provided tx hash.
   369  			wt, err := txForHash(txn, *refID)
   370  			if err != nil {
   371  				if errors.Is(err, badger.ErrKeyNotFound) {
   372  					return asset.CoinNotFoundError
   373  				}
   374  				return err
   375  			}
   376  			startNonceKey = nonceKey(wt.Nonce.Uint64())
   377  		}
   378  
   379  		it := txn.NewIterator(opts)
   380  		defer it.Close()
   381  
   382  		for it.Seek(startNonceKey); it.Valid() && (n <= 0 || len(txs) < n); it.Next() {
   383  			txHashi := it.Item()
   384  			if err := txHashi.Value(func(txHashB []byte) error {
   385  				var txHash common.Hash
   386  				copy(txHash[:], txHashB)
   387  				wt, err := txForHash(txn, txHash)
   388  				if err != nil {
   389  					return err
   390  				}
   391  				if tokenID != nil && (wt.TokenID == nil || *tokenID != *wt.TokenID) {
   392  					return nil
   393  				}
   394  				txs = append(txs, wt.WalletTransaction)
   395  				return nil
   396  			}); err != nil {
   397  				return err
   398  			}
   399  		}
   400  		return nil
   401  	})
   402  }
   403  
   404  // getPendingTxs returns a map of nonce to extendedWalletTx for all
   405  // pending transactions.
   406  func (db *badgerTxDB) getPendingTxs() ([]*extendedWalletTx, error) {
   407  	// We will be iterating backwards from the most recent nonce.
   408  	// If we find numConfirmedTxsToCheck consecutive confirmed transactions,
   409  	// we can stop iterating.
   410  	const numConfirmedTxsToCheck = 20
   411  
   412  	txs := make([]*extendedWalletTx, 0, 4)
   413  
   414  	err := db.View(func(txn *badger.Txn) error {
   415  		opts := badger.DefaultIteratorOptions
   416  		opts.Reverse = true
   417  		opts.Prefix = noncePrefix
   418  		it := txn.NewIterator(opts)
   419  		defer it.Close()
   420  
   421  		var numConfirmedTxs int
   422  		for it.Seek(maxNonceKey); it.Valid(); it.Next() {
   423  			txHashi := it.Item()
   424  			err := txHashi.Value(func(txHashB []byte) error {
   425  				var txHash common.Hash
   426  				copy(txHash[:], txHashB)
   427  				txi, err := txn.Get(txKey(txHash))
   428  				if err != nil {
   429  					return err
   430  				}
   431  				return txi.Value(func(wtB []byte) error {
   432  					wt, err := unmarshalTx(wtB)
   433  					if err != nil {
   434  						db.log.Errorf("unable to unmarhsal wallet transaction: %s: %v", string(wtB), err)
   435  						return err
   436  					}
   437  					if wt.AssumedLost {
   438  						return nil
   439  					}
   440  					if !wt.Confirmed {
   441  						numConfirmedTxs = 0
   442  						txs = append(txs, wt)
   443  					} else {
   444  						numConfirmedTxs++
   445  						if numConfirmedTxs >= numConfirmedTxsToCheck {
   446  							return nil
   447  						}
   448  					}
   449  					return nil
   450  				})
   451  
   452  			})
   453  			if err != nil {
   454  				return err
   455  			}
   456  		}
   457  		return nil
   458  	})
   459  
   460  	utils.ReverseSlice(txs)
   461  
   462  	return txs, err
   463  }
   464  
   465  // badgerLoggerWrapper wraps dex.Logger and translates Warnf to Warningf to
   466  // satisfy badger.Logger. It also lowers the log level of Infof to Debugf
   467  // and Debugf to Tracef.
   468  type badgerLoggerWrapper struct {
   469  	dex.Logger
   470  }
   471  
   472  var _ badger.Logger = (*badgerLoggerWrapper)(nil)
   473  
   474  // Debugf -> dex.Logger.Tracef
   475  func (log *badgerLoggerWrapper) Debugf(s string, a ...interface{}) {
   476  	log.Tracef(s, a...)
   477  }
   478  
   479  func (log *badgerLoggerWrapper) Debug(a ...interface{}) {
   480  	log.Trace(a...)
   481  }
   482  
   483  // Infof -> dex.Logger.Debugf
   484  func (log *badgerLoggerWrapper) Infof(s string, a ...interface{}) {
   485  	log.Debugf(s, a...)
   486  }
   487  
   488  func (log *badgerLoggerWrapper) Info(a ...interface{}) {
   489  	log.Debug(a...)
   490  }
   491  
   492  // Warningf -> dex.Logger.Warnf
   493  func (log *badgerLoggerWrapper) Warningf(s string, a ...interface{}) {
   494  	log.Warnf(s, a...)
   495  }
   496  
   497  func (log *badgerLoggerWrapper) Warning(a ...interface{}) {
   498  	log.Warn(a...)
   499  }