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 }