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 }