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  }