|
package state
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
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"
|
|
tmproto "github.com/tendermint/tendermint/proto/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))
|
|
}
|
|
|
|
// LoadStateFromDBOrGenesisFile loads the most recent state from the database,
|
|
// or creates a new one from the given genesisFilePath and persists the result
|
|
// to the database.
|
|
func LoadStateFromDBOrGenesisFile(stateDB dbm.DB, genesisFilePath string) (State, error) {
|
|
state := LoadState(stateDB)
|
|
if state.IsEmpty() {
|
|
var err error
|
|
state, err = MakeGenesisStateFromFile(genesisFilePath)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
SaveState(stateDB, state)
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// LoadStateFromDBOrGenesisDoc loads the most recent state from the database,
|
|
// or creates a new one from the given genesisDoc and persists the result
|
|
// to the database.
|
|
func LoadStateFromDBOrGenesisDoc(stateDB dbm.DB, genesisDoc *types.GenesisDoc) (State, error) {
|
|
state := LoadState(stateDB)
|
|
if state.IsEmpty() {
|
|
var err error
|
|
state, err = MakeGenesisState(genesisDoc)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
SaveState(stateDB, state)
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// LoadState loads the State from the database.
|
|
func LoadState(db dbm.DB) State {
|
|
return loadState(db, stateKey)
|
|
}
|
|
|
|
func loadState(db dbm.DB, key []byte) (state State) {
|
|
buf, err := db.Get(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(buf) == 0 {
|
|
return state
|
|
}
|
|
|
|
err = cdc.UnmarshalBinaryBare(buf, &state)
|
|
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))
|
|
}
|
|
// TODO: ensure that buf is completely read.
|
|
|
|
return state
|
|
}
|
|
|
|
// SaveState persists the State, the ValidatorsInfo, and the ConsensusParamsInfo to the database.
|
|
// This flushes the writes (e.g. calls SetSync).
|
|
func SaveState(db dbm.DB, state State) {
|
|
saveState(db, state, stateKey)
|
|
}
|
|
|
|
func saveState(db dbm.DB, state State, key []byte) {
|
|
nextHeight := state.LastBlockHeight + 1
|
|
// If first block, save validators for block 1.
|
|
if nextHeight == 1 {
|
|
// This extra logic due to Tendermint validator set changes being delayed 1 block.
|
|
// It may get overwritten due to InitChain validator updates.
|
|
lastHeightVoteChanged := int64(1)
|
|
saveValidatorsInfo(db, nextHeight, lastHeightVoteChanged, state.Validators)
|
|
}
|
|
// Save next validators.
|
|
saveValidatorsInfo(db, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators)
|
|
// Save next consensus params.
|
|
saveConsensusParamsInfo(db, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams)
|
|
err := db.SetSync(key, state.Bytes())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// BootstrapState saves a new state, used e.g. by state sync when starting from non-zero height.
|
|
func BootstrapState(db dbm.DB, state State) error {
|
|
height := state.LastBlockHeight
|
|
saveValidatorsInfo(db, height, height, state.LastValidators)
|
|
saveValidatorsInfo(db, height+1, height+1, state.Validators)
|
|
saveValidatorsInfo(db, height+2, height+2, state.NextValidators)
|
|
saveConsensusParamsInfo(db, height+1, height+1, state.ConsensusParams)
|
|
return db.SetSync(stateKey, state.Bytes())
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
// ABCIResponses retains the responses
|
|
// of the various ABCI calls during block processing.
|
|
// It is persisted to disk for each height before calling Commit.
|
|
type ABCIResponses struct {
|
|
DeliverTxs []*abci.ResponseDeliverTx `json:"deliver_txs"`
|
|
EndBlock *abci.ResponseEndBlock `json:"end_block"`
|
|
BeginBlock *abci.ResponseBeginBlock `json:"begin_block"`
|
|
}
|
|
|
|
// 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 PruneStates(db dbm.DB, from int64, to 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 := loadValidatorsInfo(db, to)
|
|
if valInfo == nil {
|
|
return fmt.Errorf("validators at height %v not found", to)
|
|
}
|
|
paramsInfo := loadConsensusParamsInfo(db, to)
|
|
if paramsInfo == nil {
|
|
return fmt.Errorf("consensus params at height %v not found", to)
|
|
}
|
|
|
|
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 := db.NewBatch()
|
|
defer batch.Close()
|
|
pruned := uint64(0)
|
|
var err error
|
|
|
|
// 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 := loadValidatorsInfo(db, h)
|
|
if v.ValidatorSet == nil {
|
|
v.ValidatorSet, err = LoadValidators(db, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.LastHeightChanged = h
|
|
batch.Set(calcValidatorsKey(h), v.Bytes())
|
|
}
|
|
} else {
|
|
batch.Delete(calcValidatorsKey(h))
|
|
}
|
|
|
|
if keepParams[h] {
|
|
p := loadConsensusParamsInfo(db, h)
|
|
if p.ConsensusParams.Equal(&tmproto.ConsensusParams{}) {
|
|
p.ConsensusParams, err = LoadConsensusParams(db, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.LastHeightChanged = h
|
|
batch.Set(calcConsensusParamsKey(h), p.Bytes())
|
|
}
|
|
} else {
|
|
batch.Delete(calcConsensusParamsKey(h))
|
|
}
|
|
|
|
batch.Delete(calcABCIResponsesKey(h))
|
|
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 = db.NewBatch()
|
|
defer batch.Close()
|
|
}
|
|
}
|
|
|
|
err = batch.WriteSync()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewABCIResponses returns a new ABCIResponses
|
|
func NewABCIResponses(block *types.Block) *ABCIResponses {
|
|
resDeliverTxs := make([]*abci.ResponseDeliverTx, len(block.Data.Txs))
|
|
if len(block.Data.Txs) == 0 {
|
|
// This makes Amino encoding/decoding consistent.
|
|
resDeliverTxs = nil
|
|
}
|
|
return &ABCIResponses{
|
|
DeliverTxs: resDeliverTxs,
|
|
}
|
|
}
|
|
|
|
// Bytes serializes the ABCIResponse using go-amino.
|
|
func (arz *ABCIResponses) Bytes() []byte {
|
|
return cdc.MustMarshalBinaryBare(arz)
|
|
}
|
|
|
|
func (arz *ABCIResponses) ResultsHash() []byte {
|
|
results := types.NewResults(arz.DeliverTxs)
|
|
return results.Hash()
|
|
}
|
|
|
|
// LoadABCIResponses loads the ABCIResponses for the given height from the database.
|
|
// This is useful for recovering from crashes where we called app.Commit and before we called
|
|
// s.Save(). It can also be used to produce Merkle proofs of the result of txs.
|
|
func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) {
|
|
buf, err := db.Get(calcABCIResponsesKey(height))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(buf) == 0 {
|
|
return nil, ErrNoABCIResponsesForHeight{height}
|
|
}
|
|
|
|
abciResponses := new(ABCIResponses)
|
|
err = cdc.UnmarshalBinaryBare(buf, abciResponses)
|
|
if err != nil {
|
|
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
|
tmos.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has
|
|
changed: %v\n`, err))
|
|
}
|
|
// TODO: ensure that buf is completely read.
|
|
|
|
return abciResponses, nil
|
|
}
|
|
|
|
// SaveABCIResponses persists the ABCIResponses 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.
|
|
//
|
|
// Exposed for testing.
|
|
func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) {
|
|
db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes())
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// ValidatorsInfo represents the latest validator set, or the last height it changed
|
|
type ValidatorsInfo struct {
|
|
ValidatorSet *types.ValidatorSet
|
|
LastHeightChanged int64
|
|
}
|
|
|
|
// Bytes serializes the ValidatorsInfo using go-amino.
|
|
func (valInfo *ValidatorsInfo) Bytes() []byte {
|
|
return cdc.MustMarshalBinaryBare(valInfo)
|
|
}
|
|
|
|
// LoadValidators loads the ValidatorSet for a given height.
|
|
// Returns ErrNoValSetForHeight if the validator set can't be found for this height.
|
|
func LoadValidators(db dbm.DB, height int64) (*types.ValidatorSet, error) {
|
|
valInfo := loadValidatorsInfo(db, height)
|
|
if valInfo == nil {
|
|
return nil, ErrNoValSetForHeight{height}
|
|
}
|
|
if valInfo.ValidatorSet == nil {
|
|
lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged)
|
|
valInfo2 := loadValidatorsInfo(db, lastStoredHeight)
|
|
if valInfo2 == nil || valInfo2.ValidatorSet == nil {
|
|
panic(
|
|
fmt.Sprintf("Couldn't find validators at height %d (height %d was originally requested)",
|
|
lastStoredHeight,
|
|
height,
|
|
),
|
|
)
|
|
}
|
|
valInfo2.ValidatorSet.IncrementProposerPriority(tmmath.SafeConvertInt32(height - lastStoredHeight)) // mutate
|
|
valInfo = valInfo2
|
|
}
|
|
|
|
return valInfo.ValidatorSet, 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) *ValidatorsInfo {
|
|
buf, err := db.Get(calcValidatorsKey(height))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(buf) == 0 {
|
|
return nil
|
|
}
|
|
|
|
v := new(ValidatorsInfo)
|
|
err = cdc.UnmarshalBinaryBare(buf, v)
|
|
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
|
|
}
|
|
|
|
// 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 saveValidatorsInfo(db dbm.DB, height, lastHeightChanged int64, valSet *types.ValidatorSet) {
|
|
if lastHeightChanged > height {
|
|
panic("LastHeightChanged cannot be greater than ValidatorsInfo height")
|
|
}
|
|
valInfo := &ValidatorsInfo{
|
|
LastHeightChanged: lastHeightChanged,
|
|
}
|
|
// Only persist validator set if it was updated or checkpoint height (see
|
|
// valSetCheckpointInterval) is reached.
|
|
if height == lastHeightChanged || height%valSetCheckpointInterval == 0 {
|
|
valInfo.ValidatorSet = valSet
|
|
}
|
|
db.Set(calcValidatorsKey(height), valInfo.Bytes())
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// ConsensusParamsInfo represents the latest consensus params, or the last height it changed
|
|
type ConsensusParamsInfo struct {
|
|
ConsensusParams tmproto.ConsensusParams
|
|
LastHeightChanged int64
|
|
}
|
|
|
|
// Bytes serializes the ConsensusParamsInfo using go-amino.
|
|
func (params ConsensusParamsInfo) Bytes() []byte {
|
|
return cdc.MustMarshalBinaryBare(params)
|
|
}
|
|
|
|
// LoadConsensusParams loads the ConsensusParams for a given height.
|
|
func LoadConsensusParams(db dbm.DB, height int64) (tmproto.ConsensusParams, error) {
|
|
empty := tmproto.ConsensusParams{}
|
|
|
|
paramsInfo := loadConsensusParamsInfo(db, height)
|
|
if paramsInfo == nil {
|
|
return empty, ErrNoConsensusParamsForHeight{height}
|
|
}
|
|
|
|
if paramsInfo.ConsensusParams.Equal(&empty) {
|
|
paramsInfo2 := loadConsensusParamsInfo(db, paramsInfo.LastHeightChanged)
|
|
if paramsInfo2 == nil {
|
|
panic(
|
|
fmt.Sprintf(
|
|
"Couldn't find consensus params at height %d as last changed from height %d",
|
|
paramsInfo.LastHeightChanged,
|
|
height,
|
|
),
|
|
)
|
|
}
|
|
paramsInfo = paramsInfo2
|
|
}
|
|
|
|
return paramsInfo.ConsensusParams, nil
|
|
}
|
|
|
|
func loadConsensusParamsInfo(db dbm.DB, height int64) *ConsensusParamsInfo {
|
|
buf, err := db.Get(calcConsensusParamsKey(height))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(buf) == 0 {
|
|
return nil
|
|
}
|
|
|
|
paramsInfo := new(ConsensusParamsInfo)
|
|
err = cdc.UnmarshalBinaryBare(buf, paramsInfo)
|
|
if 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
|
|
}
|
|
|
|
// 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 saveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params tmproto.ConsensusParams) {
|
|
paramsInfo := &ConsensusParamsInfo{
|
|
LastHeightChanged: changeHeight,
|
|
}
|
|
if changeHeight == nextHeight {
|
|
paramsInfo.ConsensusParams = params
|
|
}
|
|
db.Set(calcConsensusParamsKey(nextHeight), paramsInfo.Bytes())
|
|
}
|