github.com/jackcoble/blockbook@v0.3.2/blockbook.go (about) 1 package main 2 3 import ( 4 "blockbook/api" 5 "blockbook/bchain" 6 "blockbook/bchain/coins" 7 "blockbook/common" 8 "blockbook/db" 9 "blockbook/fiat" 10 "blockbook/server" 11 "context" 12 "encoding/json" 13 "flag" 14 "io/ioutil" 15 "log" 16 "math/rand" 17 "net/http" 18 _ "net/http/pprof" 19 "os" 20 "os/signal" 21 "runtime/debug" 22 "strings" 23 "sync/atomic" 24 "syscall" 25 "time" 26 27 "github.com/golang/glog" 28 "github.com/juju/errors" 29 ) 30 31 // debounce too close requests for resync 32 const debounceResyncIndexMs = 1009 33 34 // debounce too close requests for resync mempool (ZeroMQ sends message for each tx, when new block there are many transactions) 35 const debounceResyncMempoolMs = 1009 36 37 // store internal state about once every minute 38 const storeInternalStatePeriodMs = 59699 39 40 // exit codes from the main function 41 const exitCodeOK = 0 42 const exitCodeFatal = 255 43 44 var ( 45 blockchain = flag.String("blockchaincfg", "", "path to blockchain RPC service configuration json file") 46 47 dbPath = flag.String("datadir", "./data", "path to database directory") 48 dbCache = flag.Int("dbcache", 1<<29, "size of the rocksdb cache") 49 dbMaxOpenFiles = flag.Int("dbmaxopenfiles", 1<<14, "max open files by rocksdb") 50 51 blockFrom = flag.Int("blockheight", -1, "height of the starting block") 52 blockUntil = flag.Int("blockuntil", -1, "height of the final block") 53 rollbackHeight = flag.Int("rollback", -1, "rollback to the given height and quit") 54 55 synchronize = flag.Bool("sync", false, "synchronizes until tip, if together with zeromq, keeps index synchronized") 56 repair = flag.Bool("repair", false, "repair the database") 57 fixUtxo = flag.Bool("fixutxo", false, "check and fix utxo db and exit") 58 prof = flag.String("prof", "", "http server binding [address]:port of the interface to profiling data /debug/pprof/ (default no profiling)") 59 60 syncChunk = flag.Int("chunk", 100, "block chunk size for processing in bulk mode") 61 syncWorkers = flag.Int("workers", 8, "number of workers to process blocks in bulk mode") 62 dryRun = flag.Bool("dryrun", false, "do not index blocks, only download") 63 64 debugMode = flag.Bool("debug", false, "debug mode, return more verbose errors, reload templates on each request") 65 66 internalBinding = flag.String("internal", "", "internal http server binding [address]:port, (default no internal server)") 67 68 publicBinding = flag.String("public", "", "public http server binding [address]:port[/path] (default no public server)") 69 70 certFiles = flag.String("certfile", "", "to enable SSL specify path to certificate files without extension, expecting <certfile>.crt and <certfile>.key (default no SSL)") 71 72 explorerURL = flag.String("explorer", "", "address of blockchain explorer") 73 74 noTxCache = flag.Bool("notxcache", false, "disable tx cache") 75 76 computeColumnStats = flag.Bool("computedbstats", false, "compute column stats and exit") 77 computeFeeStatsFlag = flag.Bool("computefeestats", false, "compute fee stats for blocks in blockheight-blockuntil range and exit") 78 dbStatsPeriodHours = flag.Int("dbstatsperiod", 24, "period of db stats collection in hours, 0 disables stats collection") 79 80 // resync index at least each resyncIndexPeriodMs (could be more often if invoked by message from ZeroMQ) 81 resyncIndexPeriodMs = flag.Int("resyncindexperiod", 935093, "resync index period in milliseconds") 82 83 // resync mempool at least each resyncMempoolPeriodMs (could be more often if invoked by message from ZeroMQ) 84 resyncMempoolPeriodMs = flag.Int("resyncmempoolperiod", 60017, "resync mempool period in milliseconds") 85 ) 86 87 var ( 88 chanSyncIndex = make(chan struct{}) 89 chanSyncMempool = make(chan struct{}) 90 chanStoreInternalState = make(chan struct{}) 91 chanSyncIndexDone = make(chan struct{}) 92 chanSyncMempoolDone = make(chan struct{}) 93 chanStoreInternalStateDone = make(chan struct{}) 94 chain bchain.BlockChain 95 mempool bchain.Mempool 96 index *db.RocksDB 97 txCache *db.TxCache 98 metrics *common.Metrics 99 syncWorker *db.SyncWorker 100 internalState *common.InternalState 101 callbacksOnNewBlock []bchain.OnNewBlockFunc 102 callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc 103 callbacksOnNewFiatRatesTicker []fiat.OnNewFiatRatesTicker 104 chanOsSignal chan os.Signal 105 inShutdown int32 106 ) 107 108 func init() { 109 glog.MaxSize = 1024 * 1024 * 8 110 glog.CopyStandardLogTo("INFO") 111 } 112 113 func main() { 114 defer func() { 115 if e := recover(); e != nil { 116 glog.Error("main recovered from panic: ", e) 117 debug.PrintStack() 118 os.Exit(-1) 119 } 120 }() 121 os.Exit(mainWithExitCode()) 122 } 123 124 // allow deferred functions to run even in case of fatal error 125 func mainWithExitCode() int { 126 flag.Parse() 127 128 defer glog.Flush() 129 130 rand.Seed(time.Now().UTC().UnixNano()) 131 132 chanOsSignal = make(chan os.Signal, 1) 133 signal.Notify(chanOsSignal, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 134 135 glog.Infof("Blockbook: %+v, debug mode %v", common.GetVersionInfo(), *debugMode) 136 137 if *prof != "" { 138 go func() { 139 log.Println(http.ListenAndServe(*prof, nil)) 140 }() 141 } 142 143 if *repair { 144 if err := db.RepairRocksDB(*dbPath); err != nil { 145 glog.Errorf("RepairRocksDB %s: %v", *dbPath, err) 146 return exitCodeFatal 147 } 148 return exitCodeOK 149 } 150 151 if *blockchain == "" { 152 glog.Error("Missing blockchaincfg configuration parameter") 153 return exitCodeFatal 154 } 155 156 coin, coinShortcut, coinLabel, err := coins.GetCoinNameFromConfig(*blockchain) 157 if err != nil { 158 glog.Error("config: ", err) 159 return exitCodeFatal 160 } 161 162 // gspt.SetProcTitle("blockbook-" + normalizeName(coin)) 163 164 metrics, err = common.GetMetrics(coin) 165 if err != nil { 166 glog.Error("metrics: ", err) 167 return exitCodeFatal 168 } 169 170 if chain, mempool, err = getBlockChainWithRetry(coin, *blockchain, pushSynchronizationHandler, metrics, 120); err != nil { 171 glog.Error("rpc: ", err) 172 return exitCodeFatal 173 } 174 175 index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics) 176 if err != nil { 177 glog.Error("rocksDB: ", err) 178 return exitCodeFatal 179 } 180 defer index.Close() 181 182 internalState, err = newInternalState(coin, coinShortcut, coinLabel, index) 183 if err != nil { 184 glog.Error("internalState: ", err) 185 return exitCodeFatal 186 } 187 188 // fix possible inconsistencies in the UTXO index 189 if *fixUtxo || !internalState.UtxoChecked { 190 err = index.FixUtxos(chanOsSignal) 191 if err != nil { 192 glog.Error("fixUtxos: ", err) 193 return exitCodeFatal 194 } 195 internalState.UtxoChecked = true 196 } 197 index.SetInternalState(internalState) 198 if *fixUtxo { 199 err = index.StoreInternalState(internalState) 200 if err != nil { 201 glog.Error("StoreInternalState: ", err) 202 return exitCodeFatal 203 } 204 return exitCodeOK 205 } 206 207 if internalState.DbState != common.DbStateClosed { 208 if internalState.DbState == common.DbStateInconsistent { 209 glog.Error("internalState: database is in inconsistent state and cannot be used") 210 return exitCodeFatal 211 } 212 glog.Warning("internalState: database was left in open state, possibly previous ungraceful shutdown") 213 } 214 215 if *computeFeeStatsFlag { 216 internalState.DbState = common.DbStateOpen 217 err = computeFeeStats(chanOsSignal, *blockFrom, *blockUntil, index, chain, txCache, internalState, metrics) 218 if err != nil && err != db.ErrOperationInterrupted { 219 glog.Error("computeFeeStats: ", err) 220 return exitCodeFatal 221 } 222 return exitCodeOK 223 } 224 225 if *computeColumnStats { 226 internalState.DbState = common.DbStateOpen 227 err = index.ComputeInternalStateColumnStats(chanOsSignal) 228 if err != nil { 229 glog.Error("internalState: ", err) 230 return exitCodeFatal 231 } 232 glog.Info("DB size on disk: ", index.DatabaseSizeOnDisk(), ", DB size as computed: ", internalState.DBSizeTotal()) 233 return exitCodeOK 234 } 235 236 syncWorker, err = db.NewSyncWorker(index, chain, *syncWorkers, *syncChunk, *blockFrom, *dryRun, chanOsSignal, metrics, internalState) 237 if err != nil { 238 glog.Errorf("NewSyncWorker %v", err) 239 return exitCodeFatal 240 } 241 242 // set the DbState to open at this moment, after all important workers are initialized 243 internalState.DbState = common.DbStateOpen 244 err = index.StoreInternalState(internalState) 245 if err != nil { 246 glog.Error("internalState: ", err) 247 return exitCodeFatal 248 } 249 250 if *rollbackHeight >= 0 { 251 err = performRollback() 252 if err != nil { 253 return exitCodeFatal 254 } 255 return exitCodeOK 256 } 257 258 if txCache, err = db.NewTxCache(index, chain, metrics, internalState, !*noTxCache); err != nil { 259 glog.Error("txCache ", err) 260 return exitCodeFatal 261 } 262 263 // report BlockbookAppInfo metric, only log possible error 264 if err = blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { 265 glog.Error("blockbookAppInfoMetric ", err) 266 } 267 268 var internalServer *server.InternalServer 269 if *internalBinding != "" { 270 internalServer, err = startInternalServer() 271 if err != nil { 272 glog.Error("internal server: ", err) 273 return exitCodeFatal 274 } 275 } 276 277 var publicServer *server.PublicServer 278 if *publicBinding != "" { 279 publicServer, err = startPublicServer() 280 if err != nil { 281 glog.Error("public server: ", err) 282 return exitCodeFatal 283 } 284 } 285 286 if *synchronize { 287 internalState.SyncMode = true 288 internalState.InitialSync = true 289 if err := syncWorker.ResyncIndex(nil, true); err != nil { 290 if err != db.ErrOperationInterrupted { 291 glog.Error("resyncIndex ", err) 292 return exitCodeFatal 293 } 294 return exitCodeOK 295 } 296 // initialize mempool after the initial sync is complete 297 var addrDescForOutpoint bchain.AddrDescForOutpointFunc 298 if chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType { 299 addrDescForOutpoint = index.AddrDescForOutpoint 300 } 301 err = chain.InitializeMempool(addrDescForOutpoint, onNewTxAddr) 302 if err != nil { 303 glog.Error("initializeMempool ", err) 304 return exitCodeFatal 305 } 306 var mempoolCount int 307 if mempoolCount, err = mempool.Resync(); err != nil { 308 glog.Error("resyncMempool ", err) 309 return exitCodeFatal 310 } 311 internalState.FinishedMempoolSync(mempoolCount) 312 go syncIndexLoop() 313 go syncMempoolLoop() 314 internalState.InitialSync = false 315 } 316 go storeInternalStateLoop() 317 318 if publicServer != nil { 319 // start full public interface 320 callbacksOnNewBlock = append(callbacksOnNewBlock, publicServer.OnNewBlock) 321 callbacksOnNewTxAddr = append(callbacksOnNewTxAddr, publicServer.OnNewTxAddr) 322 callbacksOnNewFiatRatesTicker = append(callbacksOnNewFiatRatesTicker, publicServer.OnNewFiatRatesTicker) 323 publicServer.ConnectFullPublicInterface() 324 } 325 326 if *blockFrom >= 0 { 327 if *blockUntil < 0 { 328 *blockUntil = *blockFrom 329 } 330 height := uint32(*blockFrom) 331 until := uint32(*blockUntil) 332 333 if !*synchronize { 334 if err = syncWorker.ConnectBlocksParallel(height, until); err != nil { 335 if err != db.ErrOperationInterrupted { 336 glog.Error("connectBlocksParallel ", err) 337 return exitCodeFatal 338 } 339 return exitCodeOK 340 } 341 } 342 } 343 344 if internalServer != nil || publicServer != nil || chain != nil { 345 // start fiat rates downloader only if not shutting down immediately 346 initFiatRatesDownloader(index, *blockchain) 347 waitForSignalAndShutdown(internalServer, publicServer, chain, 10*time.Second) 348 } 349 350 if *synchronize { 351 close(chanSyncIndex) 352 close(chanSyncMempool) 353 close(chanStoreInternalState) 354 <-chanSyncIndexDone 355 <-chanSyncMempoolDone 356 <-chanStoreInternalStateDone 357 } 358 return exitCodeOK 359 } 360 361 func getBlockChainWithRetry(coin string, configfile string, pushHandler func(bchain.NotificationType), metrics *common.Metrics, seconds int) (bchain.BlockChain, bchain.Mempool, error) { 362 var chain bchain.BlockChain 363 var mempool bchain.Mempool 364 var err error 365 timer := time.NewTimer(time.Second) 366 for i := 0; ; i++ { 367 if chain, mempool, err = coins.NewBlockChain(coin, configfile, pushHandler, metrics); err != nil { 368 if i < seconds { 369 glog.Error("rpc: ", err, " Retrying...") 370 select { 371 case <-chanOsSignal: 372 return nil, nil, errors.New("Interrupted") 373 case <-timer.C: 374 timer.Reset(time.Second) 375 continue 376 } 377 } else { 378 return nil, nil, err 379 } 380 } 381 return chain, mempool, nil 382 } 383 } 384 385 func startInternalServer() (*server.InternalServer, error) { 386 internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, internalState) 387 if err != nil { 388 return nil, err 389 } 390 go func() { 391 err = internalServer.Run() 392 if err != nil { 393 if err.Error() == "http: Server closed" { 394 glog.Info("internal server: closed") 395 } else { 396 glog.Error(err) 397 return 398 } 399 } 400 }() 401 return internalServer, nil 402 } 403 404 func startPublicServer() (*server.PublicServer, error) { 405 // start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface 406 publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, *debugMode) 407 if err != nil { 408 return nil, err 409 } 410 go func() { 411 err = publicServer.Run() 412 if err != nil { 413 if err.Error() == "http: Server closed" { 414 glog.Info("public server: closed") 415 } else { 416 glog.Error(err) 417 return 418 } 419 } 420 }() 421 return publicServer, err 422 } 423 424 func performRollback() error { 425 bestHeight, bestHash, err := index.GetBestBlock() 426 if err != nil { 427 glog.Error("rollbackHeight: ", err) 428 return err 429 } 430 if uint32(*rollbackHeight) > bestHeight { 431 glog.Infof("nothing to rollback, rollbackHeight %d, bestHeight: %d", *rollbackHeight, bestHeight) 432 } else { 433 hashes := []string{bestHash} 434 for height := bestHeight - 1; height >= uint32(*rollbackHeight); height-- { 435 hash, err := index.GetBlockHash(height) 436 if err != nil { 437 glog.Error("rollbackHeight: ", err) 438 return err 439 } 440 hashes = append(hashes, hash) 441 } 442 err = syncWorker.DisconnectBlocks(uint32(*rollbackHeight), bestHeight, hashes) 443 if err != nil { 444 glog.Error("rollbackHeight: ", err) 445 return err 446 } 447 } 448 return nil 449 } 450 451 func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { 452 api, err := api.NewWorker(db, chain, mempool, txCache, is) 453 if err != nil { 454 return err 455 } 456 si, err := api.GetSystemInfo(false) 457 if err != nil { 458 return err 459 } 460 metrics.BlockbookAppInfo.Reset() 461 metrics.BlockbookAppInfo.With(common.Labels{ 462 "blockbook_version": si.Blockbook.Version, 463 "blockbook_commit": si.Blockbook.GitCommit, 464 "blockbook_buildtime": si.Blockbook.BuildTime, 465 "backend_version": si.Backend.Version, 466 "backend_subversion": si.Backend.Subversion, 467 "backend_protocol_version": si.Backend.ProtocolVersion}).Set(float64(0)) 468 return nil 469 } 470 471 func newInternalState(coin, coinShortcut, coinLabel string, d *db.RocksDB) (*common.InternalState, error) { 472 is, err := d.LoadInternalState(coin) 473 if err != nil { 474 return nil, err 475 } 476 is.CoinShortcut = coinShortcut 477 if coinLabel == "" { 478 coinLabel = coin 479 } 480 is.CoinLabel = coinLabel 481 name, err := os.Hostname() 482 if err != nil { 483 glog.Error("get hostname ", err) 484 } else { 485 if i := strings.IndexByte(name, '.'); i > 0 { 486 name = name[:i] 487 } 488 is.Host = name 489 } 490 return is, nil 491 } 492 493 func tickAndDebounce(tickTime time.Duration, debounceTime time.Duration, input chan struct{}, f func()) { 494 timer := time.NewTimer(tickTime) 495 var firstDebounce time.Time 496 Loop: 497 for { 498 select { 499 case _, ok := <-input: 500 if !timer.Stop() { 501 <-timer.C 502 } 503 // exit loop on closed input channel 504 if !ok { 505 break Loop 506 } 507 if firstDebounce.IsZero() { 508 firstDebounce = time.Now() 509 } 510 // debounce for up to debounceTime period 511 // afterwards execute immediately 512 if firstDebounce.Add(debounceTime).After(time.Now()) { 513 timer.Reset(debounceTime) 514 } else { 515 timer.Reset(0) 516 } 517 case <-timer.C: 518 // do the action, if not in shutdown, then start the loop again 519 if atomic.LoadInt32(&inShutdown) == 0 { 520 f() 521 } 522 timer.Reset(tickTime) 523 firstDebounce = time.Time{} 524 } 525 } 526 } 527 528 func syncIndexLoop() { 529 defer close(chanSyncIndexDone) 530 glog.Info("syncIndexLoop starting") 531 // resync index about every 15 minutes if there are no chanSyncIndex requests, with debounce 1 second 532 tickAndDebounce(time.Duration(*resyncIndexPeriodMs)*time.Millisecond, debounceResyncIndexMs*time.Millisecond, chanSyncIndex, func() { 533 if err := syncWorker.ResyncIndex(onNewBlockHash, false); err != nil { 534 glog.Error("syncIndexLoop ", errors.ErrorStack(err), ", will retry...") 535 // retry once in case of random network error, after a slight delay 536 time.Sleep(time.Millisecond * 2500) 537 if err := syncWorker.ResyncIndex(onNewBlockHash, false); err != nil { 538 glog.Error("syncIndexLoop ", errors.ErrorStack(err)) 539 } 540 } 541 }) 542 glog.Info("syncIndexLoop stopped") 543 } 544 545 func onNewBlockHash(hash string, height uint32) { 546 for _, c := range callbacksOnNewBlock { 547 c(hash, height) 548 } 549 } 550 551 func onNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { 552 for _, c := range callbacksOnNewFiatRatesTicker { 553 c(ticker) 554 } 555 } 556 557 func syncMempoolLoop() { 558 defer close(chanSyncMempoolDone) 559 glog.Info("syncMempoolLoop starting") 560 // resync mempool about every minute if there are no chanSyncMempool requests, with debounce 1 second 561 tickAndDebounce(time.Duration(*resyncMempoolPeriodMs)*time.Millisecond, debounceResyncMempoolMs*time.Millisecond, chanSyncMempool, func() { 562 internalState.StartedMempoolSync() 563 if count, err := mempool.Resync(); err != nil { 564 glog.Error("syncMempoolLoop ", errors.ErrorStack(err)) 565 } else { 566 internalState.FinishedMempoolSync(count) 567 568 } 569 }) 570 glog.Info("syncMempoolLoop stopped") 571 } 572 573 func storeInternalStateLoop() { 574 stopCompute := make(chan os.Signal) 575 defer func() { 576 close(stopCompute) 577 close(chanStoreInternalStateDone) 578 }() 579 signal.Notify(stopCompute, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 580 var computeRunning bool 581 lastCompute := time.Now() 582 lastAppInfo := time.Now() 583 logAppInfoPeriod := 15 * time.Minute 584 // randomize the duration between ComputeInternalStateColumnStats to avoid peaks after reboot of machine with multiple blockbooks 585 computePeriod := time.Duration(*dbStatsPeriodHours)*time.Hour + time.Duration(rand.Float64()*float64((4*time.Hour).Nanoseconds())) 586 if (*dbStatsPeriodHours) > 0 { 587 glog.Info("storeInternalStateLoop starting with db stats recompute period ", computePeriod) 588 } else { 589 glog.Info("storeInternalStateLoop starting with db stats compute disabled") 590 } 591 tickAndDebounce(storeInternalStatePeriodMs*time.Millisecond, (storeInternalStatePeriodMs-1)*time.Millisecond, chanStoreInternalState, func() { 592 if (*dbStatsPeriodHours) > 0 && !computeRunning && lastCompute.Add(computePeriod).Before(time.Now()) { 593 computeRunning = true 594 go func() { 595 err := index.ComputeInternalStateColumnStats(stopCompute) 596 if err != nil { 597 glog.Error("computeInternalStateColumnStats error: ", err) 598 } 599 lastCompute = time.Now() 600 computeRunning = false 601 }() 602 } 603 if err := index.StoreInternalState(internalState); err != nil { 604 glog.Error("storeInternalStateLoop ", errors.ErrorStack(err)) 605 } 606 if lastAppInfo.Add(logAppInfoPeriod).Before(time.Now()) { 607 glog.Info(index.GetMemoryStats()) 608 if err := blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { 609 glog.Error("blockbookAppInfoMetric ", err) 610 } 611 lastAppInfo = time.Now() 612 } 613 }) 614 glog.Info("storeInternalStateLoop stopped") 615 } 616 617 func onNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) { 618 for _, c := range callbacksOnNewTxAddr { 619 c(tx, desc) 620 } 621 } 622 623 func pushSynchronizationHandler(nt bchain.NotificationType) { 624 glog.V(1).Info("MQ: notification ", nt) 625 if atomic.LoadInt32(&inShutdown) != 0 { 626 return 627 } 628 if nt == bchain.NotificationNewBlock { 629 chanSyncIndex <- struct{}{} 630 } else if nt == bchain.NotificationNewTx { 631 chanSyncMempool <- struct{}{} 632 } else { 633 glog.Error("MQ: unknown notification sent") 634 } 635 } 636 637 func waitForSignalAndShutdown(internal *server.InternalServer, public *server.PublicServer, chain bchain.BlockChain, timeout time.Duration) { 638 sig := <-chanOsSignal 639 atomic.StoreInt32(&inShutdown, 1) 640 glog.Infof("shutdown: %v", sig) 641 642 ctx, cancel := context.WithTimeout(context.Background(), timeout) 643 defer cancel() 644 645 if internal != nil { 646 if err := internal.Shutdown(ctx); err != nil { 647 glog.Error("internal server: shutdown error: ", err) 648 } 649 } 650 651 if public != nil { 652 if err := public.Shutdown(ctx); err != nil { 653 glog.Error("public server: shutdown error: ", err) 654 } 655 } 656 657 if chain != nil { 658 if err := chain.Shutdown(ctx); err != nil { 659 glog.Error("rpc: shutdown error: ", err) 660 } 661 } 662 } 663 664 func printResult(txid string, vout int32, isOutput bool) error { 665 glog.Info(txid, vout, isOutput) 666 return nil 667 } 668 669 func normalizeName(s string) string { 670 s = strings.ToLower(s) 671 s = strings.Replace(s, " ", "-", -1) 672 return s 673 } 674 675 // computeFeeStats computes fee distribution in defined blocks 676 func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { 677 start := time.Now() 678 glog.Info("computeFeeStats start") 679 api, err := api.NewWorker(db, chain, mempool, txCache, is) 680 if err != nil { 681 return err 682 } 683 err = api.ComputeFeeStats(blockFrom, blockTo, stopCompute) 684 glog.Info("computeFeeStats finished in ", time.Since(start)) 685 return err 686 } 687 688 func initFiatRatesDownloader(db *db.RocksDB, configfile string) { 689 data, err := ioutil.ReadFile(configfile) 690 if err != nil { 691 glog.Errorf("Error reading file %v, %v", configfile, err) 692 return 693 } 694 695 var config struct { 696 FiatRates string `json:"fiat_rates"` 697 FiatRatesParams string `json:"fiat_rates_params"` 698 } 699 700 err = json.Unmarshal(data, &config) 701 if err != nil { 702 glog.Errorf("Error parsing config file %v, %v", configfile, err) 703 return 704 } 705 706 if config.FiatRates == "" || config.FiatRatesParams == "" { 707 glog.Infof("FiatRates config (%v) is empty, so the functionality is disabled.", configfile) 708 } else { 709 fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, nil, onNewFiatRatesTicker) 710 if err != nil { 711 glog.Errorf("NewFiatRatesDownloader Init error: %v", err) 712 return 713 } 714 glog.Infof("Starting %v FiatRates downloader...", config.FiatRates) 715 go fiatRates.Run() 716 } 717 }