Files
tendermint/state/store.go
Sergio Mena 0bc5399249 Vote extensions: add proto fields for enabling extensions (#9865)
* [cherry-picked] abci++: add proto fields for enabling vote extensions (#8587)

This pull requests adds the protocol buffer field for the `ABCI.VoteExtensionsEnableHeight` parameter. This proto field is threaded throughout all of the relevant places where consensus params are used and referenced.

This PR also adds validation of the consensus param updates. Previous consensus param changes didn't depend on _previous_ versions of the params, so this change adds a method for validating against the old params as well.

closes: #8453

* Re-sync some things with original patch

* fixes

* Remove 'Skip' from TestApp_VoteExtensions

* Fix all unit tests

* Appease linter

* Update types/params.go

Co-authored-by: Thane Thomson <connect@thanethomson.com>

Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>
Co-authored-by: Thane Thomson <connect@thanethomson.com>
2022-12-13 12:05:15 +01:00

708 lines
21 KiB
Go

package state
import (
"errors"
"fmt"
"github.com/cosmos/gogoproto/proto"
dbm "github.com/tendermint/tm-db"
abci "github.com/tendermint/tendermint/abci/types"
tmmath "github.com/tendermint/tendermint/libs/math"
tmos "github.com/tendermint/tendermint/libs/os"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/types"
)
const (
// persist validators every valSetCheckpointInterval blocks to avoid
// LoadValidators taking too much time.
// https://github.com/tendermint/tendermint/pull/3438
// 100000 results in ~ 100ms to get 100 validators (see BenchmarkLoadValidators)
valSetCheckpointInterval = 100000
)
//------------------------------------------------------------------------
func calcValidatorsKey(height int64) []byte {
return []byte(fmt.Sprintf("validatorsKey:%v", height))
}
func calcConsensusParamsKey(height int64) []byte {
return []byte(fmt.Sprintf("consensusParamsKey:%v", height))
}
func calcABCIResponsesKey(height int64) []byte {
return []byte(fmt.Sprintf("abciResponsesKey:%v", height))
}
//----------------------
var lastABCIResponseKey = []byte("lastABCIResponseKey")
//go:generate ../scripts/mockery_generate.sh Store
// Store defines the state store interface
//
// It is used to retrieve current state and save and load ABCI responses,
// validators and consensus parameters
type Store interface {
// LoadFromDBOrGenesisFile loads the most recent state.
// If the chain is new it will use the genesis file from the provided genesis file path as the current state.
LoadFromDBOrGenesisFile(string) (State, error)
// LoadFromDBOrGenesisDoc loads the most recent state.
// If the chain is new it will use the genesis doc as the current state.
LoadFromDBOrGenesisDoc(*types.GenesisDoc) (State, error)
// Load loads the current state of the blockchain
Load() (State, error)
// LoadValidators loads the validator set at a given height
LoadValidators(int64) (*types.ValidatorSet, error)
// LoadFinalizeBlockResponse loads the abciResponse for a given height
LoadFinalizeBlockResponse(int64) (*abci.ResponseFinalizeBlock, error)
// LoadLastABCIResponse loads the last abciResponse for a given height
LoadLastFinalizeBlockResponse(int64) (*abci.ResponseFinalizeBlock, error)
// LoadConsensusParams loads the consensus params for a given height
LoadConsensusParams(int64) (types.ConsensusParams, error)
// Save overwrites the previous state with the updated one
Save(State) error
// SaveFinalizeBlockResponse saves ABCIResponses for a given height
SaveFinalizeBlockResponse(int64, *abci.ResponseFinalizeBlock) error
// Bootstrap is used for bootstrapping state when not starting from a initial height.
Bootstrap(State) error
// PruneStates takes the height from which to start pruning and which height stop at
PruneStates(int64, int64, int64) error
// Close closes the connection with the database
Close() error
}
// dbStore wraps a db (github.com/tendermint/tm-db)
type dbStore struct {
db dbm.DB
StoreOptions
}
type StoreOptions struct {
// DiscardABCIResponses determines whether or not the store
// retains all ABCIResponses. If DiscardABCIResponses is enabled,
// the store will maintain only the response object from the latest
// height.
DiscardABCIResponses bool
}
var _ Store = (*dbStore)(nil)
// NewStore creates the dbStore of the state pkg.
func NewStore(db dbm.DB, options StoreOptions) Store {
return dbStore{db, options}
}
// LoadStateFromDBOrGenesisFile loads the most recent state from the database,
// or creates a new one from the given genesisFilePath.
func (store dbStore) LoadFromDBOrGenesisFile(genesisFilePath string) (State, error) {
state, err := store.Load()
if err != nil {
return State{}, err
}
if state.IsEmpty() {
var err error
state, err = MakeGenesisStateFromFile(genesisFilePath)
if err != nil {
return state, err
}
}
return state, nil
}
// LoadStateFromDBOrGenesisDoc loads the most recent state from the database,
// or creates a new one from the given genesisDoc.
func (store dbStore) LoadFromDBOrGenesisDoc(genesisDoc *types.GenesisDoc) (State, error) {
state, err := store.Load()
if err != nil {
return State{}, err
}
if state.IsEmpty() {
var err error
state, err = MakeGenesisState(genesisDoc)
if err != nil {
return state, err
}
}
return state, nil
}
// LoadState loads the State from the database.
func (store dbStore) Load() (State, error) {
return store.loadState(stateKey)
}
func (store dbStore) loadState(key []byte) (state State, err error) {
buf, err := store.db.Get(key)
if err != nil {
return state, err
}
if len(buf) == 0 {
return state, nil
}
sp := new(tmstate.State)
err = proto.Unmarshal(buf, sp)
if err != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadState: Data has been corrupted or its spec has changed:
%v\n`, err))
}
sm, err := FromProto(sp)
if err != nil {
return state, err
}
return *sm, nil
}
// Save persists the State, the ValidatorsInfo, and the ConsensusParamsInfo to the database.
// This flushes the writes (e.g. calls SetSync).
func (store dbStore) Save(state State) error {
return store.save(state, stateKey)
}
func (store dbStore) save(state State, key []byte) error {
nextHeight := state.LastBlockHeight + 1
// If first block, save validators for the block.
if nextHeight == 1 {
nextHeight = state.InitialHeight
// This extra logic due to Tendermint validator set changes being delayed 1 block.
// It may get overwritten due to InitChain validator updates.
if err := store.saveValidatorsInfo(nextHeight, nextHeight, state.Validators); err != nil {
return err
}
}
// Save next validators.
if err := store.saveValidatorsInfo(nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators); err != nil {
return err
}
// Save next consensus params.
if err := store.saveConsensusParamsInfo(nextHeight,
state.LastHeightConsensusParamsChanged, state.ConsensusParams); err != nil {
return err
}
err := store.db.SetSync(key, state.Bytes())
if err != nil {
return err
}
return nil
}
// BootstrapState saves a new state, used e.g. by state sync when starting from non-zero height.
func (store dbStore) Bootstrap(state State) error {
height := state.LastBlockHeight + 1
if height == 1 {
height = state.InitialHeight
}
if height > 1 && !state.LastValidators.IsNilOrEmpty() {
if err := store.saveValidatorsInfo(height-1, height-1, state.LastValidators); err != nil {
return err
}
}
if err := store.saveValidatorsInfo(height, height, state.Validators); err != nil {
return err
}
if err := store.saveValidatorsInfo(height+1, height+1, state.NextValidators); err != nil {
return err
}
if err := store.saveConsensusParamsInfo(height,
state.LastHeightConsensusParamsChanged, state.ConsensusParams); err != nil {
return err
}
return store.db.SetSync(stateKey, state.Bytes())
}
// PruneStates deletes states between the given heights (including from, excluding to). It is not
// guaranteed to delete all states, since the last checkpointed state and states being pointed to by
// e.g. `LastHeightChanged` must remain. The state at to must also exist.
//
// The from parameter is necessary since we can't do a key scan in a performant way due to the key
// encoding not preserving ordering: https://github.com/tendermint/tendermint/issues/4567
// This will cause some old states to be left behind when doing incremental partial prunes,
// specifically older checkpoints and LastHeightChanged targets.
func (store dbStore) PruneStates(from int64, to int64, evidenceThresholdHeight int64) error {
if from <= 0 || to <= 0 {
return fmt.Errorf("from height %v and to height %v must be greater than 0", from, to)
}
if from >= to {
return fmt.Errorf("from height %v must be lower than to height %v", from, to)
}
valInfo, err := loadValidatorsInfo(store.db, min(to, evidenceThresholdHeight))
if err != nil {
return fmt.Errorf("validators at height %v not found: %w", to, err)
}
paramsInfo, err := store.loadConsensusParamsInfo(to)
if err != nil {
return fmt.Errorf("consensus params at height %v not found: %w", to, err)
}
keepVals := make(map[int64]bool)
if valInfo.ValidatorSet == nil {
keepVals[valInfo.LastHeightChanged] = true
keepVals[lastStoredHeightFor(to, valInfo.LastHeightChanged)] = true // keep last checkpoint too
}
keepParams := make(map[int64]bool)
if paramsInfo.ConsensusParams.Equal(&tmproto.ConsensusParams{}) {
keepParams[paramsInfo.LastHeightChanged] = true
}
batch := store.db.NewBatch()
defer batch.Close()
pruned := uint64(0)
// We have to delete in reverse order, to avoid deleting previous heights that have validator
// sets and consensus params that we may need to retrieve.
for h := to - 1; h >= from; h-- {
// For heights we keep, we must make sure they have the full validator set or consensus
// params, otherwise they will panic if they're retrieved directly (instead of
// indirectly via a LastHeightChanged pointer).
if keepVals[h] {
v, err := loadValidatorsInfo(store.db, h)
if err != nil || v.ValidatorSet == nil {
vip, err := store.LoadValidators(h)
if err != nil {
return err
}
pvi, err := vip.ToProto()
if err != nil {
return err
}
v.ValidatorSet = pvi
v.LastHeightChanged = h
bz, err := v.Marshal()
if err != nil {
return err
}
err = batch.Set(calcValidatorsKey(h), bz)
if err != nil {
return err
}
}
} else if h < evidenceThresholdHeight {
err = batch.Delete(calcValidatorsKey(h))
if err != nil {
return err
}
}
// else we keep the validator set because we might need
// it later on for evidence verification
if keepParams[h] {
p, err := store.loadConsensusParamsInfo(h)
if err != nil {
return err
}
if p.ConsensusParams.Equal(&tmproto.ConsensusParams{}) {
params, err := store.LoadConsensusParams(h)
if err != nil {
return err
}
p.ConsensusParams = params.ToProto()
p.LastHeightChanged = h
bz, err := p.Marshal()
if err != nil {
return err
}
err = batch.Set(calcConsensusParamsKey(h), bz)
if err != nil {
return err
}
}
} else {
err = batch.Delete(calcConsensusParamsKey(h))
if err != nil {
return err
}
}
err = batch.Delete(calcABCIResponsesKey(h))
if err != nil {
return err
}
pruned++
// avoid batches growing too large by flushing to database regularly
if pruned%1000 == 0 && pruned > 0 {
err := batch.Write()
if err != nil {
return err
}
batch.Close()
batch = store.db.NewBatch()
defer batch.Close()
}
}
err = batch.WriteSync()
if err != nil {
return err
}
return nil
}
//------------------------------------------------------------------------
// TxResultsHash returns the root hash of a Merkle tree of
// ExecTxResulst responses (see ABCIResults.Hash)
//
// See merkle.SimpleHashFromByteSlices
func TxResultsHash(txResults []*abci.ExecTxResult) []byte {
return types.NewResults(txResults).Hash()
}
// LoadFinalizeBlockResponse loads the DiscardABCIResponses for the given height from the
// database. If the node has D set to true, ErrABCIResponsesNotPersisted
// is persisted. If not found, ErrNoABCIResponsesForHeight is returned.
func (store dbStore) LoadFinalizeBlockResponse(height int64) (*abci.ResponseFinalizeBlock, error) {
if store.DiscardABCIResponses {
return nil, ErrFinalizeBlockResponsesNotPersisted
}
buf, err := store.db.Get(calcABCIResponsesKey(height))
if err != nil {
return nil, err
}
if len(buf) == 0 {
return nil, ErrNoABCIResponsesForHeight{height}
}
resp := new(abci.ResponseFinalizeBlock)
err = resp.Unmarshal(buf)
if err != nil {
// The data might be of the legacy ABCI response type, so
// we try to unmarshal that
legacyResp := new(tmstate.LegacyABCIResponses)
rerr := legacyResp.Unmarshal(buf)
if rerr != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadFinalizeBlockResponse: Data has been corrupted or its spec has
changed: %v\n`, err))
}
// The state store contains the old format. Migrate to
// the new ResponseFinalizeBlock format. Note that the
// new struct expects the AgreedAppData which we don't have.
return responseFinalizeBlockFromLegacy(legacyResp), nil
}
// TODO: ensure that buf is completely read.
return resp, nil
}
// LoadLastFinalizeBlockResponses loads the FinalizeBlockResponses from the most recent height.
// The height parameter is used to ensure that the response corresponds to the latest height.
// If not, an error is returned.
//
// This method is used for recovering in the case that we called the Commit ABCI
// method on the application but crashed before persisting the results.
func (store dbStore) LoadLastFinalizeBlockResponse(height int64) (*abci.ResponseFinalizeBlock, error) {
bz, err := store.db.Get(lastABCIResponseKey)
if err != nil {
return nil, err
}
if len(bz) == 0 {
return nil, errors.New("no last ABCI response has been persisted")
}
info := new(tmstate.ABCIResponsesInfo)
err = info.Unmarshal(bz)
if err != nil {
tmos.Exit(fmt.Sprintf(`LoadLastFinalizeBlockResponse: Data has been corrupted or its spec has
changed: %v\n`, err))
}
// Here we validate the result by comparing its height to the expected height.
if height != info.GetHeight() {
return nil, fmt.Errorf("expected height %d but last stored abci responses was at height %d", height, info.GetHeight())
}
// It is possible if this is called directly after an upgrade that
// ResponseFinalizeBlock is nil. In which case we use the legacy
// ABCI responses
if info.ResponseFinalizeBlock == nil {
// sanity check
if info.LegacyAbciResponses == nil {
panic("state store contains last abci response but it is empty")
}
return responseFinalizeBlockFromLegacy(info.LegacyAbciResponses), nil
}
return info.ResponseFinalizeBlock, nil
}
// SaveFinalizeBlockResponse persists the ResponseFinalizeBlock to the database.
// This is useful in case we crash after app.Commit and before s.Save().
// Responses are indexed by height so they can also be loaded later to produce
// Merkle proofs.
//
// CONTRACT: height must be monotonically increasing every time this is called.
func (store dbStore) SaveFinalizeBlockResponse(height int64, resp *abci.ResponseFinalizeBlock) error {
var dtxs []*abci.ExecTxResult
// strip nil values,
for _, tx := range resp.TxResults {
if tx != nil {
dtxs = append(dtxs, tx)
}
}
resp.TxResults = dtxs
// If the flag is false then we save the ABCIResponse. This can be used for the /BlockResults
// query or to reindex an event using the command line.
if !store.DiscardABCIResponses {
bz, err := resp.Marshal()
if err != nil {
return err
}
if err := store.db.Set(calcABCIResponsesKey(height), bz); err != nil {
return err
}
}
// We always save the last ABCI response for crash recovery.
// This overwrites the previous saved ABCI Response.
response := &tmstate.ABCIResponsesInfo{
ResponseFinalizeBlock: resp,
Height: height,
}
bz, err := response.Marshal()
if err != nil {
return err
}
return store.db.SetSync(lastABCIResponseKey, bz)
}
//-----------------------------------------------------------------------------
// LoadValidators loads the ValidatorSet for a given height.
// Returns ErrNoValSetForHeight if the validator set can't be found for this height.
func (store dbStore) LoadValidators(height int64) (*types.ValidatorSet, error) {
valInfo, err := loadValidatorsInfo(store.db, height)
if err != nil {
return nil, ErrNoValSetForHeight{height}
}
if valInfo.ValidatorSet == nil {
lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged)
valInfo2, err := loadValidatorsInfo(store.db, lastStoredHeight)
if err != nil || valInfo2.ValidatorSet == nil {
return nil,
fmt.Errorf("couldn't find validators at height %d (height %d was originally requested): %w",
lastStoredHeight,
height,
err,
)
}
vs, err := types.ValidatorSetFromProto(valInfo2.ValidatorSet)
if err != nil {
return nil, err
}
vs.IncrementProposerPriority(tmmath.SafeConvertInt32(height - lastStoredHeight)) // mutate
vi2, err := vs.ToProto()
if err != nil {
return nil, err
}
valInfo2.ValidatorSet = vi2
valInfo = valInfo2
}
vip, err := types.ValidatorSetFromProto(valInfo.ValidatorSet)
if err != nil {
return nil, err
}
return vip, nil
}
func lastStoredHeightFor(height, lastHeightChanged int64) int64 {
checkpointHeight := height - height%valSetCheckpointInterval
return tmmath.MaxInt64(checkpointHeight, lastHeightChanged)
}
// CONTRACT: Returned ValidatorsInfo can be mutated.
func loadValidatorsInfo(db dbm.DB, height int64) (*tmstate.ValidatorsInfo, error) {
buf, err := db.Get(calcValidatorsKey(height))
if err != nil {
return nil, err
}
if len(buf) == 0 {
return nil, errors.New("value retrieved from db is empty")
}
v := new(tmstate.ValidatorsInfo)
err = v.Unmarshal(buf)
if err != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadValidators: Data has been corrupted or its spec has changed:
%v\n`, err))
}
// TODO: ensure that buf is completely read.
return v, nil
}
// saveValidatorsInfo persists the validator set.
//
// `height` is the effective height for which the validator is responsible for
// signing. It should be called from s.Save(), right before the state itself is
// persisted.
func (store dbStore) saveValidatorsInfo(height, lastHeightChanged int64, valSet *types.ValidatorSet) error {
if lastHeightChanged > height {
return errors.New("lastHeightChanged cannot be greater than ValidatorsInfo height")
}
valInfo := &tmstate.ValidatorsInfo{
LastHeightChanged: lastHeightChanged,
}
// Only persist validator set if it was updated or checkpoint height (see
// valSetCheckpointInterval) is reached.
if height == lastHeightChanged || height%valSetCheckpointInterval == 0 {
pv, err := valSet.ToProto()
if err != nil {
return err
}
valInfo.ValidatorSet = pv
}
bz, err := valInfo.Marshal()
if err != nil {
return err
}
err = store.db.Set(calcValidatorsKey(height), bz)
if err != nil {
return err
}
return nil
}
//-----------------------------------------------------------------------------
// ConsensusParamsInfo represents the latest consensus params, or the last height it changed
// LoadConsensusParams loads the ConsensusParams for a given height.
func (store dbStore) LoadConsensusParams(height int64) (types.ConsensusParams, error) {
var (
empty = types.ConsensusParams{}
emptypb = tmproto.ConsensusParams{}
)
paramsInfo, err := store.loadConsensusParamsInfo(height)
if err != nil {
return empty, fmt.Errorf("could not find consensus params for height #%d: %w", height, err)
}
if paramsInfo.ConsensusParams.Equal(&emptypb) {
paramsInfo2, err := store.loadConsensusParamsInfo(paramsInfo.LastHeightChanged)
if err != nil {
return empty, fmt.Errorf(
"couldn't find consensus params at height %d as last changed from height %d: %w",
paramsInfo.LastHeightChanged,
height,
err,
)
}
paramsInfo = paramsInfo2
}
return types.ConsensusParamsFromProto(paramsInfo.ConsensusParams), nil
}
func (store dbStore) loadConsensusParamsInfo(height int64) (*tmstate.ConsensusParamsInfo, error) {
buf, err := store.db.Get(calcConsensusParamsKey(height))
if err != nil {
return nil, err
}
if len(buf) == 0 {
return nil, errors.New("value retrieved from db is empty")
}
paramsInfo := new(tmstate.ConsensusParamsInfo)
if err = paramsInfo.Unmarshal(buf); err != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadConsensusParams: Data has been corrupted or its spec has changed:
%v\n`, err))
}
// TODO: ensure that buf is completely read.
return paramsInfo, nil
}
// saveConsensusParamsInfo persists the consensus params for the next block to disk.
// It should be called from s.Save(), right before the state itself is persisted.
// If the consensus params did not change after processing the latest block,
// only the last height for which they changed is persisted.
func (store dbStore) saveConsensusParamsInfo(nextHeight, changeHeight int64, params types.ConsensusParams) error {
paramsInfo := &tmstate.ConsensusParamsInfo{
LastHeightChanged: changeHeight,
}
if changeHeight == nextHeight {
paramsInfo.ConsensusParams = params.ToProto()
}
bz, err := paramsInfo.Marshal()
if err != nil {
return err
}
err = store.db.Set(calcConsensusParamsKey(nextHeight), bz)
if err != nil {
return err
}
return nil
}
func (store dbStore) Close() error {
return store.db.Close()
}
func min(a int64, b int64) int64 {
if a < b {
return a
}
return b
}
// responseFinalizeBlockFromLegacy is a convenience function that takes the old abci responses and morphs
// it to the finalize block response. Note that the agreed app data is missing
func responseFinalizeBlockFromLegacy(legacyResp *tmstate.LegacyABCIResponses) *abci.ResponseFinalizeBlock {
return &abci.ResponseFinalizeBlock{
TxResults: legacyResp.DeliverTxs,
ValidatorUpdates: legacyResp.EndBlock.ValidatorUpdates,
ConsensusParamUpdates: legacyResp.EndBlock.ConsensusParamUpdates,
Events: append(legacyResp.BeginBlock.Events, legacyResp.EndBlock.Events...),
// NOTE: AgreedAppData is missing in the response but will
// be caught and filled in consensus/replay.go
}
}