github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/tslib/tsdb.go (about)

     1  package tslib
     2  
     3  import (
     4  	"encoding/binary"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  
    10  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    11  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    12  )
    13  
    14  type TimestampRecord struct {
    15  	Bn uint32 `json:"bn"`
    16  	Ts uint32 `json:"ts"`
    17  }
    18  
    19  type TimestampDatabase struct {
    20  	loaded bool
    21  	count  uint64
    22  	memory []TimestampRecord
    23  }
    24  
    25  var perChainTimestamps = map[string]TimestampDatabase{}
    26  
    27  // NTimestamps returns the number of records in the timestamp file
    28  func NTimestamps(chain string) (base.Blknum, error) {
    29  	if perChainTimestamps[chain].count > 0 {
    30  		return base.Blknum(perChainTimestamps[chain].count), nil
    31  	}
    32  
    33  	tsPath := config.PathToTimestamps(chain)
    34  
    35  	fileStat, err := os.Stat(tsPath)
    36  	if err != nil {
    37  		return 0, err
    38  	}
    39  
    40  	perChainTimestamps[chain] = TimestampDatabase{
    41  		loaded: perChainTimestamps[chain].loaded,
    42  		count:  uint64(fileStat.Size()) / 8,
    43  		memory: perChainTimestamps[chain].memory,
    44  	}
    45  	return base.Blknum(perChainTimestamps[chain].count), nil
    46  }
    47  
    48  // loadTimestamps loads the timestamp data from the file into memory. If the timestamps are already loaded, we short circiut.
    49  func loadTimestamps(chain string) error {
    50  	if perChainTimestamps[chain].loaded {
    51  		return nil
    52  	}
    53  
    54  	cnt, err := NTimestamps(chain)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	tsPath := config.PathToTimestamps(chain)
    60  	tsFile, err := os.OpenFile(tsPath, os.O_RDONLY, 0)
    61  	if err != nil {
    62  		return err
    63  	}
    64  	defer tsFile.Close()
    65  
    66  	memory := make([]TimestampRecord, cnt)
    67  	err = binary.Read(tsFile, binary.LittleEndian, memory)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	perChainTimestamps[chain] = TimestampDatabase{
    73  		loaded: true,
    74  		count:  perChainTimestamps[chain].count,
    75  		memory: memory,
    76  	}
    77  
    78  	return nil
    79  }
    80  
    81  var ErrInTheFuture = errors.New("timestamp in the future")
    82  
    83  // FromTs is a local function that returns a Timestamp record given a Unix timestamp. It
    84  // loads the timestamp file into memory if it isn't already. If the timestamp requested
    85  // is past the end of the timestamp file, it estimates the block number and returns and error
    86  func FromTs(chain string, ts base.Timestamp) (*TimestampRecord, error) {
    87  	cnt, err := NTimestamps(chain)
    88  	if err != nil {
    89  		return &TimestampRecord{}, err
    90  	}
    91  
    92  	err = loadTimestamps(chain)
    93  	if err != nil {
    94  		return &TimestampRecord{}, err
    95  	}
    96  
    97  	if ts > base.Timestamp(perChainTimestamps[chain].memory[cnt-1].Ts) {
    98  		last := perChainTimestamps[chain].memory[cnt-1]
    99  		secs := ts - base.Timestamp(last.Ts)
   100  		blks := uint32(float64(secs) / 13.3)
   101  		last.Bn = last.Bn + blks
   102  		last.Ts = uint32(ts)
   103  		return &last, ErrInTheFuture
   104  	}
   105  
   106  	// Go docs: Search uses binary search to find and return the smallest index i in [0, n) at which f(i) is true,
   107  	index := sort.Search(int(cnt), func(i int) bool {
   108  		d := perChainTimestamps[chain].memory[i]
   109  		v := base.Timestamp(d.Ts)
   110  		return v > ts
   111  	})
   112  
   113  	// ts should not be before the first block
   114  	if index == 0 {
   115  		return nil, errors.New("timestamp is before the first block")
   116  	}
   117  
   118  	// The index is one past where we want to be because it's the first block larger
   119  	index--
   120  
   121  	return &perChainTimestamps[chain].memory[index], nil
   122  }
   123  
   124  func ClearCache(chain string) {
   125  	perChainTimestamps[chain] = TimestampDatabase{
   126  		loaded: false,
   127  		count:  0,
   128  		memory: nil,
   129  	}
   130  }
   131  
   132  // FromBn is a local function that returns a Timestamp record given a blockNum. It
   133  // loads the timestamp file into memory if it isn't already loaded
   134  func FromBn(chain string, bn base.Blknum) (*TimestampRecord, error) {
   135  	cnt, err := NTimestamps(chain)
   136  	if err != nil {
   137  		return &TimestampRecord{}, err
   138  	}
   139  
   140  	if bn >= base.Blknum(cnt) {
   141  		return &TimestampRecord{}, errors.New("invalid block number " + fmt.Sprintf("%d of %d", bn, cnt))
   142  	}
   143  
   144  	err = loadTimestamps(chain)
   145  	if err != nil {
   146  		return &TimestampRecord{}, err
   147  	}
   148  
   149  	return &perChainTimestamps[chain].memory[bn], nil
   150  }