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 }