github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/light/store/db/db.go (about) 1 package db 2 3 import ( 4 "encoding/binary" 5 "fmt" 6 "regexp" 7 "strconv" 8 9 dbm "github.com/badrootd/nibiru-db" 10 11 cmtsync "github.com/badrootd/nibiru-cometbft/libs/sync" 12 "github.com/badrootd/nibiru-cometbft/light/store" 13 cmtproto "github.com/badrootd/nibiru-cometbft/proto/tendermint/types" 14 "github.com/badrootd/nibiru-cometbft/types" 15 ) 16 17 var sizeKey = []byte("size") 18 19 type dbs struct { 20 db dbm.DB 21 prefix string 22 23 mtx cmtsync.RWMutex 24 size uint16 25 } 26 27 // New returns a Store that wraps any DB (with an optional prefix in case you 28 // want to use one DB with many light clients). 29 func New(db dbm.DB, prefix string) store.Store { 30 size := uint16(0) 31 bz, err := db.Get(sizeKey) 32 if err == nil && len(bz) > 0 { 33 size = unmarshalSize(bz) 34 } 35 36 return &dbs{db: db, prefix: prefix, size: size} 37 } 38 39 // SaveLightBlock persists LightBlock to the db. 40 // 41 // Safe for concurrent use by multiple goroutines. 42 func (s *dbs) SaveLightBlock(lb *types.LightBlock) error { 43 if lb.Height <= 0 { 44 panic("negative or zero height") 45 } 46 47 lbpb, err := lb.ToProto() 48 if err != nil { 49 return fmt.Errorf("unable to convert light block to protobuf: %w", err) 50 } 51 52 lbBz, err := lbpb.Marshal() 53 if err != nil { 54 return fmt.Errorf("marshaling LightBlock: %w", err) 55 } 56 57 s.mtx.Lock() 58 defer s.mtx.Unlock() 59 60 b := s.db.NewBatch() 61 defer b.Close() 62 if err = b.Set(s.lbKey(lb.Height), lbBz); err != nil { 63 return err 64 } 65 if err = b.Set(sizeKey, marshalSize(s.size+1)); err != nil { 66 return err 67 } 68 if err = b.WriteSync(); err != nil { 69 return err 70 } 71 s.size++ 72 73 return nil 74 } 75 76 // DeleteLightBlockAndValidatorSet deletes the LightBlock from 77 // the db. 78 // 79 // Safe for concurrent use by multiple goroutines. 80 func (s *dbs) DeleteLightBlock(height int64) error { 81 if height <= 0 { 82 panic("negative or zero height") 83 } 84 85 s.mtx.Lock() 86 defer s.mtx.Unlock() 87 88 b := s.db.NewBatch() 89 defer b.Close() 90 if err := b.Delete(s.lbKey(height)); err != nil { 91 return err 92 } 93 if err := b.Set(sizeKey, marshalSize(s.size-1)); err != nil { 94 return err 95 } 96 if err := b.WriteSync(); err != nil { 97 return err 98 } 99 s.size-- 100 101 return nil 102 } 103 104 // LightBlock retrieves the LightBlock at the given height. 105 // 106 // Safe for concurrent use by multiple goroutines. 107 func (s *dbs) LightBlock(height int64) (*types.LightBlock, error) { 108 if height <= 0 { 109 panic("negative or zero height") 110 } 111 112 bz, err := s.db.Get(s.lbKey(height)) 113 if err != nil { 114 panic(err) 115 } 116 if len(bz) == 0 { 117 return nil, store.ErrLightBlockNotFound 118 } 119 120 var lbpb cmtproto.LightBlock 121 err = lbpb.Unmarshal(bz) 122 if err != nil { 123 return nil, fmt.Errorf("unmarshal error: %w", err) 124 } 125 126 lightBlock, err := types.LightBlockFromProto(&lbpb) 127 if err != nil { 128 return nil, fmt.Errorf("proto conversion error: %w", err) 129 } 130 131 return lightBlock, err 132 } 133 134 // LastLightBlockHeight returns the last LightBlock height stored. 135 // 136 // Safe for concurrent use by multiple goroutines. 137 func (s *dbs) LastLightBlockHeight() (int64, error) { 138 itr, err := s.db.ReverseIterator( 139 s.lbKey(1), 140 append(s.lbKey(1<<63-1), byte(0x00)), 141 ) 142 if err != nil { 143 panic(err) 144 } 145 defer itr.Close() 146 147 for itr.Valid() { 148 key := itr.Key() 149 _, height, ok := parseLbKey(key) 150 if ok { 151 return height, nil 152 } 153 itr.Next() 154 } 155 156 return -1, itr.Error() 157 } 158 159 // FirstLightBlockHeight returns the first LightBlock height stored. 160 // 161 // Safe for concurrent use by multiple goroutines. 162 func (s *dbs) FirstLightBlockHeight() (int64, error) { 163 itr, err := s.db.Iterator( 164 s.lbKey(1), 165 append(s.lbKey(1<<63-1), byte(0x00)), 166 ) 167 if err != nil { 168 panic(err) 169 } 170 defer itr.Close() 171 172 for itr.Valid() { 173 key := itr.Key() 174 _, height, ok := parseLbKey(key) 175 if ok { 176 return height, nil 177 } 178 itr.Next() 179 } 180 181 return -1, itr.Error() 182 } 183 184 // LightBlockBefore iterates over light blocks until it finds a block before 185 // the given height. It returns ErrLightBlockNotFound if no such block exists. 186 // 187 // Safe for concurrent use by multiple goroutines. 188 func (s *dbs) LightBlockBefore(height int64) (*types.LightBlock, error) { 189 if height <= 0 { 190 panic("negative or zero height") 191 } 192 193 itr, err := s.db.ReverseIterator( 194 s.lbKey(1), 195 s.lbKey(height), 196 ) 197 if err != nil { 198 panic(err) 199 } 200 defer itr.Close() 201 202 for itr.Valid() { 203 key := itr.Key() 204 _, existingHeight, ok := parseLbKey(key) 205 if ok { 206 return s.LightBlock(existingHeight) 207 } 208 itr.Next() 209 } 210 if err = itr.Error(); err != nil { 211 return nil, err 212 } 213 214 return nil, store.ErrLightBlockNotFound 215 } 216 217 // Prune prunes header & validator set pairs until there are only size pairs 218 // left. 219 // 220 // Safe for concurrent use by multiple goroutines. 221 func (s *dbs) Prune(size uint16) error { 222 // 1) Check how many we need to prune. 223 s.mtx.RLock() 224 sSize := s.size 225 s.mtx.RUnlock() 226 227 if sSize <= size { // nothing to prune 228 return nil 229 } 230 numToPrune := sSize - size 231 232 // 2) Iterate over headers and perform a batch operation. 233 itr, err := s.db.Iterator( 234 s.lbKey(1), 235 append(s.lbKey(1<<63-1), byte(0x00)), 236 ) 237 if err != nil { 238 return err 239 } 240 defer itr.Close() 241 242 b := s.db.NewBatch() 243 defer b.Close() 244 245 pruned := 0 246 for itr.Valid() && numToPrune > 0 { 247 key := itr.Key() 248 _, height, ok := parseLbKey(key) 249 if ok { 250 if err = b.Delete(s.lbKey(height)); err != nil { 251 return err 252 } 253 } 254 itr.Next() 255 numToPrune-- 256 pruned++ 257 } 258 if err = itr.Error(); err != nil { 259 return err 260 } 261 262 err = b.WriteSync() 263 if err != nil { 264 return err 265 } 266 267 // 3) Update size. 268 s.mtx.Lock() 269 defer s.mtx.Unlock() 270 271 s.size -= uint16(pruned) 272 273 if wErr := s.db.SetSync(sizeKey, marshalSize(s.size)); wErr != nil { 274 return fmt.Errorf("failed to persist size: %w", wErr) 275 } 276 277 return nil 278 } 279 280 // Size returns the number of header & validator set pairs. 281 // 282 // Safe for concurrent use by multiple goroutines. 283 func (s *dbs) Size() uint16 { 284 s.mtx.RLock() 285 defer s.mtx.RUnlock() 286 return s.size 287 } 288 289 func (s *dbs) lbKey(height int64) []byte { 290 return []byte(fmt.Sprintf("lb/%s/%020d", s.prefix, height)) 291 } 292 293 var keyPattern = regexp.MustCompile(`^(lb)/([^/]*)/([0-9]+)$`) 294 295 func parseKey(key []byte) (part string, prefix string, height int64, ok bool) { 296 submatch := keyPattern.FindSubmatch(key) 297 if submatch == nil { 298 return "", "", 0, false 299 } 300 part = string(submatch[1]) 301 prefix = string(submatch[2]) 302 height, err := strconv.ParseInt(string(submatch[3]), 10, 64) 303 if err != nil { 304 return "", "", 0, false 305 } 306 ok = true // good! 307 return 308 } 309 310 func parseLbKey(key []byte) (prefix string, height int64, ok bool) { 311 var part string 312 part, prefix, height, ok = parseKey(key) 313 if part != "lb" { 314 return "", 0, false 315 } 316 return 317 } 318 319 func marshalSize(size uint16) []byte { 320 bs := make([]byte, 2) 321 binary.LittleEndian.PutUint16(bs, size) 322 return bs 323 } 324 325 func unmarshalSize(bz []byte) uint16 { 326 return binary.LittleEndian.Uint16(bz) 327 }