package lite import ( "fmt" "regexp" "strconv" amino "github.com/tendermint/go-amino" cryptoamino "github.com/tendermint/tendermint/crypto/encoding/amino" log "github.com/tendermint/tendermint/libs/log" lerr "github.com/tendermint/tendermint/lite/errors" "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-db" ) var _ PersistentProvider = (*DBProvider)(nil) // DBProvider stores commits and validator sets in a DB. type DBProvider struct { logger log.Logger label string db dbm.DB cdc *amino.Codec limit int } func NewDBProvider(label string, db dbm.DB) *DBProvider { // NOTE: when debugging, this type of construction might be useful. //db = dbm.NewDebugDB("db provider "+tmrand.Str(4), db) cdc := amino.NewCodec() cryptoamino.RegisterAmino(cdc) dbp := &DBProvider{ logger: log.NewNopLogger(), label: label, db: db, cdc: cdc, } return dbp } func (dbp *DBProvider) SetLogger(logger log.Logger) { dbp.logger = logger.With("label", dbp.label) } func (dbp *DBProvider) SetLimit(limit int) *DBProvider { dbp.limit = limit return dbp } // Implements PersistentProvider. func (dbp *DBProvider) SaveFullCommit(fc FullCommit) error { dbp.logger.Info("DBProvider.SaveFullCommit()...", "fc", fc) batch := dbp.db.NewBatch() defer batch.Close() // Save the fc.validators. // We might be overwriting what we already have, but // it makes the logic easier for now. vsKey := validatorSetKey(fc.ChainID(), fc.Height()) vsBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.Validators) if err != nil { return err } batch.Set(vsKey, vsBz) // Save the fc.NextValidators. nvsKey := validatorSetKey(fc.ChainID(), fc.Height()+1) nvsBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.NextValidators) if err != nil { return err } batch.Set(nvsKey, nvsBz) // Save the fc.SignedHeader shKey := signedHeaderKey(fc.ChainID(), fc.Height()) shBz, err := dbp.cdc.MarshalBinaryLengthPrefixed(fc.SignedHeader) if err != nil { return err } batch.Set(shKey, shBz) // And write sync. batch.WriteSync() // Garbage collect. // TODO: optimize later. if dbp.limit > 0 { dbp.deleteAfterN(fc.ChainID(), dbp.limit) } return nil } // Implements Provider. func (dbp *DBProvider) LatestFullCommit(chainID string, minHeight, maxHeight int64) ( FullCommit, error) { dbp.logger.Info("DBProvider.LatestFullCommit()...", "chainID", chainID, "minHeight", minHeight, "maxHeight", maxHeight) if minHeight <= 0 { minHeight = 1 } if maxHeight == 0 { maxHeight = 1<<63 - 1 } itr := dbp.db.ReverseIterator( signedHeaderKey(chainID, minHeight), append(signedHeaderKey(chainID, maxHeight), byte(0x00)), ) defer itr.Close() for itr.Valid() { key := itr.Key() _, _, ok := parseSignedHeaderKey(key) if !ok { // Skip over other keys. itr.Next() continue } else { // Found the latest full commit signed header. shBz := itr.Value() sh := types.SignedHeader{} err := dbp.cdc.UnmarshalBinaryLengthPrefixed(shBz, &sh) if err != nil { return FullCommit{}, err } lfc, err := dbp.fillFullCommit(sh) if err == nil { dbp.logger.Info("DBProvider.LatestFullCommit() found latest.", "height", lfc.Height()) return lfc, nil } dbp.logger.Error("DBProvider.LatestFullCommit() got error", "lfc", lfc) dbp.logger.Error(fmt.Sprintf("%+v", err)) return lfc, err } } return FullCommit{}, lerr.ErrCommitNotFound() } func (dbp *DBProvider) ValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { return dbp.getValidatorSet(chainID, height) } func (dbp *DBProvider) getValidatorSet(chainID string, height int64) (valset *types.ValidatorSet, err error) { vsBz := dbp.db.Get(validatorSetKey(chainID, height)) if vsBz == nil { err = lerr.ErrUnknownValidators(chainID, height) return } err = dbp.cdc.UnmarshalBinaryLengthPrefixed(vsBz, &valset) if err != nil { return } // To test deep equality. This makes it easier to test for e.g. valset // equivalence using assert.Equal (tests for deep equality) in our tests, // which also tests for unexported/private field equivalence. valset.TotalVotingPower() return } func (dbp *DBProvider) fillFullCommit(sh types.SignedHeader) (FullCommit, error) { var chainID = sh.ChainID var height = sh.Height var valset, nextValset *types.ValidatorSet // Load the validator set. valset, err := dbp.getValidatorSet(chainID, height) if err != nil { return FullCommit{}, err } // Load the next validator set. nextValset, err = dbp.getValidatorSet(chainID, height+1) if err != nil { return FullCommit{}, err } // Return filled FullCommit. return FullCommit{ SignedHeader: sh, Validators: valset, NextValidators: nextValset, }, nil } func (dbp *DBProvider) deleteAfterN(chainID string, after int) error { dbp.logger.Info("DBProvider.deleteAfterN()...", "chainID", chainID, "after", after) itr := dbp.db.ReverseIterator( signedHeaderKey(chainID, 1), append(signedHeaderKey(chainID, 1<<63-1), byte(0x00)), ) defer itr.Close() var lastHeight int64 = 1<<63 - 1 var numSeen = 0 var numDeleted = 0 for itr.Valid() { key := itr.Key() _, height, ok := parseChainKeyPrefix(key) if !ok { return fmt.Errorf("unexpected key %v", key) } if height < lastHeight { lastHeight = height numSeen++ } if numSeen > after { dbp.db.Delete(key) numDeleted++ } itr.Next() } dbp.logger.Info(fmt.Sprintf("DBProvider.deleteAfterN() deleted %v items", numDeleted)) return nil } //---------------------------------------- // key encoding func signedHeaderKey(chainID string, height int64) []byte { return []byte(fmt.Sprintf("%s/%010d/sh", chainID, height)) } func validatorSetKey(chainID string, height int64) []byte { return []byte(fmt.Sprintf("%s/%010d/vs", chainID, height)) } //---------------------------------------- // key parsing var keyPattern = regexp.MustCompile(`^([^/]+)/([0-9]*)/(.*)$`) func parseKey(key []byte) (chainID string, height int64, part string, ok bool) { submatch := keyPattern.FindSubmatch(key) if submatch == nil { return "", 0, "", false } chainID = string(submatch[1]) heightStr := string(submatch[2]) heightInt, err := strconv.Atoi(heightStr) if err != nil { return "", 0, "", false } height = int64(heightInt) part = string(submatch[3]) ok = true // good! return } func parseSignedHeaderKey(key []byte) (chainID string, height int64, ok bool) { var part string chainID, height, part, ok = parseKey(key) if part != "sh" { return "", 0, false } return } func parseChainKeyPrefix(key []byte) (chainID string, height int64, ok bool) { chainID, height, _, ok = parseKey(key) return }