mirror of
https://github.com/trezor/blockbook.git
synced 2026-03-03 22:34:32 +01:00
383 lines
14 KiB
Go
383 lines
14 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
const (
|
|
// DbStateClosed means db was closed gracefully
|
|
DbStateClosed = uint32(iota)
|
|
// DbStateOpen means db is open or application died without closing the db
|
|
DbStateOpen
|
|
// DbStateInconsistent means db is in inconsistent state and cannot be used
|
|
DbStateInconsistent
|
|
)
|
|
|
|
var inShutdown int32
|
|
|
|
// InternalStateColumn contains the data of a db column
|
|
type InternalStateColumn struct {
|
|
Name string `json:"name" ts_doc:"Name of the database column."`
|
|
Version uint32 `json:"version" ts_doc:"Version or schema version of the column."`
|
|
Rows int64 `json:"rows" ts_doc:"Number of rows stored in this column."`
|
|
KeyBytes int64 `json:"keyBytes" ts_doc:"Total size (in bytes) of keys stored in this column."`
|
|
ValueBytes int64 `json:"valueBytes" ts_doc:"Total size (in bytes) of values stored in this column."`
|
|
Updated time.Time `json:"updated" ts_doc:"Timestamp of the last update to this column."`
|
|
}
|
|
|
|
// BackendInfo is used to get information about blockchain
|
|
type BackendInfo struct {
|
|
BackendError string `json:"error,omitempty" ts_doc:"Error message if something went wrong in the backend."`
|
|
Chain string `json:"chain,omitempty" ts_doc:"Name of the chain - e.g. 'main'."`
|
|
Blocks int `json:"blocks,omitempty" ts_doc:"Number of fully verified blocks in the chain."`
|
|
Headers int `json:"headers,omitempty" ts_doc:"Number of block headers in the chain."`
|
|
BestBlockHash string `json:"bestBlockHash,omitempty" ts_doc:"Hash of the best block in hex."`
|
|
Difficulty string `json:"difficulty,omitempty" ts_doc:"Current difficulty of the network."`
|
|
SizeOnDisk int64 `json:"sizeOnDisk,omitempty" ts_doc:"Size of the blockchain data on disk in bytes."`
|
|
Version string `json:"version,omitempty" ts_doc:"Version of the blockchain backend - e.g. '280000'."`
|
|
Subversion string `json:"subversion,omitempty" ts_doc:"Subversion of the blockchain backend - e.g. '/Satoshi:28.0.0/'."`
|
|
ProtocolVersion string `json:"protocolVersion,omitempty" ts_doc:"Protocol version of the blockchain backend - e.g. '70016'."`
|
|
Timeoffset float64 `json:"timeOffset,omitempty" ts_doc:"Time offset (in seconds) reported by the backend."`
|
|
Warnings string `json:"warnings,omitempty" ts_doc:"Any warnings given by the backend regarding the chain state."`
|
|
ConsensusVersion string `json:"consensus_version,omitempty" ts_doc:"Version or details of the consensus protocol in use."`
|
|
Consensus interface{} `json:"consensus,omitempty" ts_doc:"Additional chain-specific consensus data."`
|
|
}
|
|
|
|
// InternalState contains the data of the internal state
|
|
type InternalState struct {
|
|
mux sync.Mutex `ts_doc:"Mutex for synchronized access to the internal state."`
|
|
|
|
Coin string `json:"coin" ts_doc:"Coin name (e.g. 'Bitcoin')."`
|
|
CoinShortcut string `json:"coinShortcut" ts_doc:"Short code for the coin (e.g. 'BTC')."`
|
|
CoinLabel string `json:"coinLabel" ts_doc:"Human-readable label for the coin (e.g. 'Bitcoin main')."`
|
|
Host string `json:"host" ts_doc:"Hostname of the node or backend."`
|
|
Network string `json:"network,omitempty" ts_doc:"Network name if different from CoinShortcut (e.g. 'testnet')."`
|
|
|
|
DbState uint32 `json:"dbState" ts_doc:"State of the database (closed=0, open=1, inconsistent=2)."`
|
|
ExtendedIndex bool `json:"extendedIndex" ts_doc:"Indicates if an extended indexing strategy is used."`
|
|
|
|
LastStore time.Time `json:"lastStore" ts_doc:"Time when the internal state was last stored/persisted."`
|
|
|
|
// true if application is with flag --sync
|
|
SyncMode bool `json:"syncMode" ts_doc:"Flag indicating if the node is in sync mode."`
|
|
|
|
InitialSync bool `json:"initialSync" ts_doc:"If true, the system is in the initial sync phase."`
|
|
IsSynchronized bool `json:"isSynchronized" ts_doc:"If true, the main index is fully synced to BestHeight."`
|
|
BestHeight uint32 `json:"bestHeight" ts_doc:"Current best block height known to the indexer."`
|
|
StartSync time.Time `json:"-" ts_doc:"Timestamp when sync started (not exposed via JSON)."`
|
|
LastSync time.Time `json:"lastSync" ts_doc:"Timestamp of the last successful sync."`
|
|
BlockTimes []uint32 `json:"-" ts_doc:"List of block timestamps (per height) for calculating historical stats (not exposed via JSON)."`
|
|
AvgBlockPeriod uint32 `json:"-" ts_doc:"Average time (in seconds) per block for the last 100 blocks (not exposed via JSON)."`
|
|
|
|
IsMempoolSynchronized bool `json:"isMempoolSynchronized" ts_doc:"If true, mempool data is in sync."`
|
|
MempoolSize int `json:"mempoolSize" ts_doc:"Number of transactions in the current mempool."`
|
|
LastMempoolSync time.Time `json:"lastMempoolSync" ts_doc:"Timestamp of the last mempool sync."`
|
|
|
|
DbColumns []InternalStateColumn `json:"dbColumns" ts_doc:"List of database column statistics."`
|
|
|
|
HasFiatRates bool `json:"-" ts_doc:"True if fiat rates are supported (not exposed via JSON)."`
|
|
HasTokenFiatRates bool `json:"-" ts_doc:"True if token fiat rates are supported (not exposed via JSON)."`
|
|
HistoricalFiatRatesTime time.Time `json:"historicalFiatRatesTime" ts_doc:"Timestamp of the last historical fiat rates update."`
|
|
HistoricalTokenFiatRatesTime time.Time `json:"historicalTokenFiatRatesTime" ts_doc:"Timestamp of the last historical token fiat rates update."`
|
|
|
|
EnableSubNewTx bool `json:"-" ts_doc:"Internal flag controlling subscription to new transactions (not exposed)."`
|
|
|
|
BackendInfo BackendInfo `json:"-" ts_doc:"Information about the connected blockchain backend (not exposed in JSON)."`
|
|
|
|
// database migrations
|
|
UtxoChecked bool `json:"utxoChecked" ts_doc:"Indicates if UTXO consistency checks have been performed."`
|
|
SortedAddressContracts bool `json:"sortedAddressContracts" ts_doc:"Indicates if address/contract sorting has been completed."`
|
|
|
|
// golomb filter settings
|
|
BlockGolombFilterP uint8 `json:"block_golomb_filter_p" ts_doc:"Parameter P for building Golomb-Rice filters for blocks."`
|
|
BlockFilterScripts string `json:"block_filter_scripts" ts_doc:"Scripts included in block filters (e.g., 'p2pkh,p2sh')."`
|
|
BlockFilterUseZeroedKey bool `json:"block_filter_use_zeroed_key" ts_doc:"If true, uses a zeroed key for building block filters."`
|
|
|
|
// allowed number of fetched accounts over websocket
|
|
WsGetAccountInfoLimit int `json:"-" ts_doc:"Limit of how many getAccountInfo calls can be made via WS (not exposed)."`
|
|
WsLimitExceedingIPs map[string]int `json:"-" ts_doc:"Tracks IP addresses exceeding the WS limit (not exposed)."`
|
|
}
|
|
|
|
// StartedSync signals start of synchronization
|
|
func (is *InternalState) StartedSync() {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.StartSync = time.Now().UTC()
|
|
is.IsSynchronized = false
|
|
}
|
|
|
|
// FinishedSync marks end of synchronization, bestHeight specifies new best block height
|
|
func (is *InternalState) FinishedSync(bestHeight uint32) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.IsSynchronized = true
|
|
is.BestHeight = bestHeight
|
|
is.LastSync = time.Now().UTC()
|
|
}
|
|
|
|
// UpdateBestHeight sets new best height, without changing IsSynchronized flag
|
|
func (is *InternalState) UpdateBestHeight(bestHeight uint32) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.BestHeight = bestHeight
|
|
is.LastSync = time.Now().UTC()
|
|
}
|
|
|
|
// FinishedSyncNoChange marks end of synchronization in case no index update was necessary, it does not update lastSync time
|
|
func (is *InternalState) FinishedSyncNoChange() {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.IsSynchronized = true
|
|
}
|
|
|
|
// GetSyncState gets the state of synchronization
|
|
func (is *InternalState) GetSyncState() (bool, uint32, time.Time, time.Time) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
return is.IsSynchronized, is.BestHeight, is.LastSync, is.StartSync
|
|
}
|
|
|
|
// StartedMempoolSync signals start of mempool synchronization
|
|
func (is *InternalState) StartedMempoolSync() {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.IsMempoolSynchronized = false
|
|
}
|
|
|
|
// FinishedMempoolSync marks end of mempool synchronization
|
|
func (is *InternalState) FinishedMempoolSync(mempoolSize int) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.IsMempoolSynchronized = true
|
|
is.MempoolSize = mempoolSize
|
|
is.LastMempoolSync = time.Now()
|
|
}
|
|
|
|
// GetMempoolSyncState gets the state of mempool synchronization
|
|
func (is *InternalState) GetMempoolSyncState() (bool, time.Time, int) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
return is.IsMempoolSynchronized, is.LastMempoolSync, is.MempoolSize
|
|
}
|
|
|
|
// AddDBColumnStats adds differences in column statistics to column stats
|
|
func (is *InternalState) AddDBColumnStats(c int, rowsDiff int64, keyBytesDiff int64, valueBytesDiff int64) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
dc := &is.DbColumns[c]
|
|
dc.Rows += rowsDiff
|
|
dc.KeyBytes += keyBytesDiff
|
|
dc.ValueBytes += valueBytesDiff
|
|
dc.Updated = time.Now()
|
|
}
|
|
|
|
// SetDBColumnStats sets new values of column stats
|
|
func (is *InternalState) SetDBColumnStats(c int, rows int64, keyBytes int64, valueBytes int64) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
dc := &is.DbColumns[c]
|
|
dc.Rows = rows
|
|
dc.KeyBytes = keyBytes
|
|
dc.ValueBytes = valueBytes
|
|
dc.Updated = time.Now()
|
|
}
|
|
|
|
// GetDBColumnStatValues gets stat values for given column
|
|
func (is *InternalState) GetDBColumnStatValues(c int) (int64, int64, int64) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if c < len(is.DbColumns) {
|
|
return is.DbColumns[c].Rows, is.DbColumns[c].KeyBytes, is.DbColumns[c].ValueBytes
|
|
}
|
|
return 0, 0, 0
|
|
}
|
|
|
|
// GetAllDBColumnStats returns stats for all columns
|
|
func (is *InternalState) GetAllDBColumnStats() []InternalStateColumn {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
return slices.Clone(is.DbColumns)
|
|
}
|
|
|
|
// DBSizeTotal sums the computed sizes of all columns
|
|
func (is *InternalState) DBSizeTotal() int64 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
total := int64(0)
|
|
for _, c := range is.DbColumns {
|
|
total += c.KeyBytes + c.ValueBytes
|
|
}
|
|
return total
|
|
}
|
|
|
|
// GetBlockTime returns block time if block found or 0
|
|
func (is *InternalState) GetBlockTime(height uint32) uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if int(height) < len(is.BlockTimes) {
|
|
return is.BlockTimes[height]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// GetLastBlockTime returns time of the last block
|
|
func (is *InternalState) GetLastBlockTime() uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if len(is.BlockTimes) > 0 {
|
|
return is.BlockTimes[len(is.BlockTimes)-1]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// SetBlockTimes initializes BlockTimes array, returns AvgBlockPeriod
|
|
func (is *InternalState) SetBlockTimes(blockTimes []uint32) uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if len(is.BlockTimes) < len(blockTimes) {
|
|
// no new block was set
|
|
is.BlockTimes = blockTimes
|
|
} else {
|
|
copy(is.BlockTimes, blockTimes)
|
|
}
|
|
is.computeAvgBlockPeriod()
|
|
glog.Info("set ", len(is.BlockTimes), " block times, average block period ", is.AvgBlockPeriod, "s")
|
|
return is.AvgBlockPeriod
|
|
}
|
|
|
|
// SetBlockTime sets block time to BlockTimes, allocating the slice as necessary, returns AvgBlockPeriod
|
|
func (is *InternalState) SetBlockTime(height uint32, time uint32) uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if int(height) >= len(is.BlockTimes) {
|
|
extend := int(height) - len(is.BlockTimes) + 1
|
|
for i := 0; i < extend; i++ {
|
|
is.BlockTimes = append(is.BlockTimes, time)
|
|
}
|
|
} else {
|
|
is.BlockTimes[height] = time
|
|
}
|
|
is.computeAvgBlockPeriod()
|
|
return is.AvgBlockPeriod
|
|
}
|
|
|
|
// RemoveLastBlockTimes removes last times from BlockTimes
|
|
func (is *InternalState) RemoveLastBlockTimes(count int) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
if len(is.BlockTimes) < count {
|
|
count = len(is.BlockTimes)
|
|
}
|
|
is.BlockTimes = is.BlockTimes[:len(is.BlockTimes)-count]
|
|
is.computeAvgBlockPeriod()
|
|
}
|
|
|
|
// GetBlockHeightOfTime returns block height of the first block with time greater or equal to the given time or MaxUint32 if no such block
|
|
func (is *InternalState) GetBlockHeightOfTime(time uint32) uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
height := sort.Search(len(is.BlockTimes), func(i int) bool { return time <= is.BlockTimes[i] })
|
|
if height == len(is.BlockTimes) {
|
|
return ^uint32(0)
|
|
}
|
|
// as the block times can sometimes be out of order try 20 blocks lower to locate a block with the time greater or equal to the given time
|
|
max, height := height, height-20
|
|
if height < 0 {
|
|
height = 0
|
|
}
|
|
for ; height <= max; height++ {
|
|
if time <= is.BlockTimes[height] {
|
|
break
|
|
}
|
|
}
|
|
return uint32(height)
|
|
}
|
|
|
|
const avgBlockPeriodSample = 100
|
|
|
|
// Avg100BlocksPeriod returns average period of the last 100 blocks in seconds
|
|
func (is *InternalState) GetAvgBlockPeriod() uint32 {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
return is.AvgBlockPeriod
|
|
}
|
|
|
|
// computeAvgBlockPeriod returns computes average of the last 100 blocks in seconds
|
|
func (is *InternalState) computeAvgBlockPeriod() {
|
|
last := len(is.BlockTimes) - 1
|
|
first := last - avgBlockPeriodSample - 1
|
|
if first < 0 {
|
|
return
|
|
}
|
|
is.AvgBlockPeriod = (is.BlockTimes[last] - is.BlockTimes[first]) / avgBlockPeriodSample
|
|
}
|
|
|
|
// GetNetwork returns network. If not set returns the same value as CoinShortcut
|
|
func (is *InternalState) GetNetwork() string {
|
|
network := is.Network
|
|
if network == "" {
|
|
return is.CoinShortcut
|
|
}
|
|
return network
|
|
}
|
|
|
|
// SetBackendInfo sets new BackendInfo
|
|
func (is *InternalState) SetBackendInfo(bi *BackendInfo) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.BackendInfo = *bi
|
|
}
|
|
|
|
// GetBackendInfo gets BackendInfo
|
|
func (is *InternalState) GetBackendInfo() BackendInfo {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
return is.BackendInfo
|
|
}
|
|
|
|
// Pack marshals internal state to json
|
|
func (is *InternalState) Pack() ([]byte, error) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.LastStore = time.Now()
|
|
return json.Marshal(is)
|
|
}
|
|
|
|
// UnpackInternalState unmarshals internal state from json
|
|
func UnpackInternalState(buf []byte) (*InternalState, error) {
|
|
var is InternalState
|
|
if err := json.Unmarshal(buf, &is); err != nil {
|
|
return nil, err
|
|
}
|
|
return &is, nil
|
|
}
|
|
|
|
// SetInShutdown sets the internal state to in shutdown state
|
|
func SetInShutdown() {
|
|
atomic.StoreInt32(&inShutdown, 1)
|
|
}
|
|
|
|
// IsInShutdown returns true if in application shutdown state
|
|
func IsInShutdown() bool {
|
|
return atomic.LoadInt32(&inShutdown) != 0
|
|
}
|
|
|
|
func (is *InternalState) AddWsLimitExceedingIP(ip string) {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.WsLimitExceedingIPs[ip] = is.WsLimitExceedingIPs[ip] + 1
|
|
}
|
|
|
|
func (is *InternalState) ResetWsLimitExceedingIPs() {
|
|
is.mux.Lock()
|
|
defer is.mux.Unlock()
|
|
is.WsLimitExceedingIPs = make(map[string]int)
|
|
}
|