mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-09 13:30:11 +00:00
This pull request merges in the changes for implementing Proposer-based timestamps into `master`. The power was primarily being done in the `wb/proposer-based-timestamps` branch, with changes being merged into that branch during development. This pull request represents an amalgamation of the changes made into that development branch. All of the changes that were placed into that branch have been cleanly rebased on top of the latest `master`. The changes compile and the tests pass insofar as our tests in general pass. ### Note To Reviewers These changes have been extensively reviewed during development. There is not much new here. In the interest of making effective use of time, I would recommend against trying to perform a complete audit of the changes presented and instead examine for mistakes that may have occurred during the process of rebasing the changes. I gave the complete change set a first pass for any issues, but additional eyes would be very appreciated. In sum, this change set does the following: closes #6942 merges in #6849
322 lines
11 KiB
Go
322 lines
11 KiB
Go
package state_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/config"
|
|
"github.com/tendermint/tendermint/crypto"
|
|
"github.com/tendermint/tendermint/crypto/ed25519"
|
|
sm "github.com/tendermint/tendermint/internal/state"
|
|
"github.com/tendermint/tendermint/internal/test/factory"
|
|
tmrand "github.com/tendermint/tendermint/libs/rand"
|
|
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
const (
|
|
// make sure this is the same as in state/store.go
|
|
valSetCheckpointInterval = 100000
|
|
)
|
|
|
|
func TestStoreBootstrap(t *testing.T) {
|
|
stateDB := dbm.NewMemDB()
|
|
stateStore := sm.NewStore(stateDB)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
val, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
val2, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
val3, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
vals := types.NewValidatorSet([]*types.Validator{val, val2, val3})
|
|
bootstrapState := makeRandomStateFromValidatorSet(vals, 100, 100)
|
|
require.NoError(t, stateStore.Bootstrap(bootstrapState))
|
|
|
|
// bootstrap should also save the previous validator
|
|
_, err = stateStore.LoadValidators(99)
|
|
require.NoError(t, err)
|
|
|
|
_, err = stateStore.LoadValidators(100)
|
|
require.NoError(t, err)
|
|
|
|
_, err = stateStore.LoadValidators(101)
|
|
require.NoError(t, err)
|
|
|
|
state, err := stateStore.Load()
|
|
require.NoError(t, err)
|
|
require.Equal(t, bootstrapState, state)
|
|
}
|
|
|
|
func TestStoreLoadValidators(t *testing.T) {
|
|
stateDB := dbm.NewMemDB()
|
|
stateStore := sm.NewStore(stateDB)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
val, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
val2, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
val3, _, err := factory.Validator(ctx, 10+int64(rand.Uint32()))
|
|
require.NoError(t, err)
|
|
vals := types.NewValidatorSet([]*types.Validator{val, val2, val3})
|
|
|
|
// 1) LoadValidators loads validators using a height where they were last changed
|
|
// Note that only the next validators at height h + 1 are saved
|
|
require.NoError(t, stateStore.Save(makeRandomStateFromValidatorSet(vals, 1, 1)))
|
|
require.NoError(t, stateStore.Save(makeRandomStateFromValidatorSet(vals.CopyIncrementProposerPriority(1), 2, 1)))
|
|
loadedVals, err := stateStore.LoadValidators(3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, vals.CopyIncrementProposerPriority(3), loadedVals)
|
|
|
|
// 2) LoadValidators loads validators using a checkpoint height
|
|
|
|
// add a validator set at the checkpoint
|
|
err = stateStore.Save(makeRandomStateFromValidatorSet(vals, valSetCheckpointInterval, 1))
|
|
require.NoError(t, err)
|
|
|
|
// check that a request will go back to the last checkpoint
|
|
_, err = stateStore.LoadValidators(valSetCheckpointInterval + 1)
|
|
require.Error(t, err)
|
|
require.Equal(t, fmt.Sprintf("couldn't find validators at height %d (height %d was originally requested): "+
|
|
"value retrieved from db is empty",
|
|
valSetCheckpointInterval, valSetCheckpointInterval+1), err.Error())
|
|
|
|
// now save a validator set at that checkpoint
|
|
err = stateStore.Save(makeRandomStateFromValidatorSet(vals, valSetCheckpointInterval-1, 1))
|
|
require.NoError(t, err)
|
|
|
|
loadedVals, err = stateStore.LoadValidators(valSetCheckpointInterval)
|
|
require.NoError(t, err)
|
|
// validator set gets updated with the one given hence we expect it to equal next validators (with an increment of one)
|
|
// as opposed to being equal to an increment of 100000 - 1 (if we didn't save at the checkpoint)
|
|
require.Equal(t, vals.CopyIncrementProposerPriority(2), loadedVals)
|
|
require.NotEqual(t, vals.CopyIncrementProposerPriority(valSetCheckpointInterval), loadedVals)
|
|
}
|
|
|
|
// This benchmarks the speed of loading validators from different heights if there is no validator set change.
|
|
// NOTE: This isn't too indicative of validator retrieval speed as the db is always (regardless of height) only
|
|
// performing two operations: 1) retrieve validator info at height x, which has a last validator set change of 1
|
|
// and 2) retrieve the validator set at the aforementioned height 1.
|
|
func BenchmarkLoadValidators(b *testing.B) {
|
|
const valSetSize = 100
|
|
|
|
cfg, err := config.ResetTestRoot("state_")
|
|
require.NoError(b, err)
|
|
|
|
defer os.RemoveAll(cfg.RootDir)
|
|
dbType := dbm.BackendType(cfg.DBBackend)
|
|
stateDB, err := dbm.NewDB("state", dbType, cfg.DBDir())
|
|
require.NoError(b, err)
|
|
stateStore := sm.NewStore(stateDB)
|
|
state, err := sm.MakeGenesisStateFromFile(cfg.GenesisFile())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
state.Validators = genValSet(valSetSize)
|
|
state.NextValidators = state.Validators.CopyIncrementProposerPriority(1)
|
|
err = stateStore.Save(state)
|
|
require.NoError(b, err)
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 10; i < 10000000000; i *= 10 { // 10, 100, 1000, ...
|
|
i := i
|
|
err = stateStore.Save(makeRandomStateFromValidatorSet(state.NextValidators,
|
|
int64(i)-1, state.LastHeightValidatorsChanged))
|
|
if err != nil {
|
|
b.Fatalf("error saving store: %v", err)
|
|
}
|
|
|
|
b.Run(fmt.Sprintf("height=%d", i), func(b *testing.B) {
|
|
for n := 0; n < b.N; n++ {
|
|
_, err := stateStore.LoadValidators(int64(i))
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStoreLoadConsensusParams(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
stateDB := dbm.NewMemDB()
|
|
stateStore := sm.NewStore(stateDB)
|
|
err := stateStore.Save(makeRandomStateFromConsensusParams(ctx, t, types.DefaultConsensusParams(), 1, 1))
|
|
require.NoError(t, err)
|
|
params, err := stateStore.LoadConsensusParams(1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.DefaultConsensusParams(), ¶ms)
|
|
|
|
// we give the state store different params but say that the height hasn't changed, hence
|
|
// it should save a pointer to the params at height 1
|
|
differentParams := types.DefaultConsensusParams()
|
|
differentParams.Block.MaxBytes = 20000
|
|
err = stateStore.Save(makeRandomStateFromConsensusParams(ctx, t, differentParams, 10, 1))
|
|
require.NoError(t, err)
|
|
res, err := stateStore.LoadConsensusParams(10)
|
|
require.NoError(t, err)
|
|
require.Equal(t, res, params)
|
|
require.NotEqual(t, res, differentParams)
|
|
}
|
|
|
|
func TestPruneStates(t *testing.T) {
|
|
testcases := map[string]struct {
|
|
startHeight int64
|
|
endHeight int64
|
|
pruneHeight int64
|
|
expectErr bool
|
|
remainingValSetHeight int64
|
|
remainingParamsHeight int64
|
|
}{
|
|
"error when prune height is 0": {1, 100, 0, true, 0, 0},
|
|
"error when prune height is negative": {1, 100, -10, true, 0, 0},
|
|
"error when prune height does not exist": {1, 100, 101, true, 0, 0},
|
|
"prune all": {1, 100, 100, false, 93, 95},
|
|
"prune from non 1 height": {10, 50, 40, false, 33, 35},
|
|
"prune some": {1, 10, 8, false, 3, 5},
|
|
// we test this because we flush to disk every 1000 "states"
|
|
"prune more than 1000 state": {1, 1010, 1010, false, 1003, 1005},
|
|
"prune across checkpoint": {99900, 100002, 100002, false, 100000, 99995},
|
|
}
|
|
for name, tc := range testcases {
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
db := dbm.NewMemDB()
|
|
|
|
stateStore := sm.NewStore(db)
|
|
pk := ed25519.GenPrivKey().PubKey()
|
|
|
|
// Generate a bunch of state data. Validators change for heights ending with 3, and
|
|
// parameters when ending with 5.
|
|
validator := &types.Validator{Address: tmrand.Bytes(crypto.AddressSize), VotingPower: 100, PubKey: pk}
|
|
validatorSet := &types.ValidatorSet{
|
|
Validators: []*types.Validator{validator},
|
|
Proposer: validator,
|
|
}
|
|
valsChanged := int64(0)
|
|
paramsChanged := int64(0)
|
|
|
|
for h := tc.startHeight; h <= tc.endHeight; h++ {
|
|
if valsChanged == 0 || h%10 == 2 {
|
|
valsChanged = h + 1 // Have to add 1, since NextValidators is what's stored
|
|
}
|
|
if paramsChanged == 0 || h%10 == 5 {
|
|
paramsChanged = h
|
|
}
|
|
|
|
state := sm.State{
|
|
InitialHeight: 1,
|
|
LastBlockHeight: h - 1,
|
|
Validators: validatorSet,
|
|
NextValidators: validatorSet,
|
|
ConsensusParams: types.ConsensusParams{
|
|
Block: types.BlockParams{MaxBytes: 10e6},
|
|
},
|
|
LastHeightValidatorsChanged: valsChanged,
|
|
LastHeightConsensusParamsChanged: paramsChanged,
|
|
}
|
|
|
|
if state.LastBlockHeight >= 1 {
|
|
state.LastValidators = state.Validators
|
|
}
|
|
|
|
err := stateStore.Save(state)
|
|
require.NoError(t, err)
|
|
|
|
err = stateStore.SaveABCIResponses(h, &tmstate.ABCIResponses{
|
|
DeliverTxs: []*abci.ResponseDeliverTx{
|
|
{Data: []byte{1}},
|
|
{Data: []byte{2}},
|
|
{Data: []byte{3}},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Test assertions
|
|
err := stateStore.PruneStates(tc.pruneHeight)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
for h := tc.pruneHeight; h <= tc.endHeight; h++ {
|
|
vals, err := stateStore.LoadValidators(h)
|
|
require.NoError(t, err, h)
|
|
require.NotNil(t, vals, h)
|
|
|
|
params, err := stateStore.LoadConsensusParams(h)
|
|
require.NoError(t, err, h)
|
|
require.NotNil(t, params, h)
|
|
|
|
abci, err := stateStore.LoadABCIResponses(h)
|
|
require.NoError(t, err, h)
|
|
require.NotNil(t, abci, h)
|
|
}
|
|
|
|
emptyParams := types.ConsensusParams{}
|
|
|
|
for h := tc.startHeight; h < tc.pruneHeight; h++ {
|
|
vals, err := stateStore.LoadValidators(h)
|
|
if h == tc.remainingValSetHeight {
|
|
require.NoError(t, err, h)
|
|
require.NotNil(t, vals, h)
|
|
} else {
|
|
require.Error(t, err, h)
|
|
require.Nil(t, vals, h)
|
|
}
|
|
|
|
params, err := stateStore.LoadConsensusParams(h)
|
|
if h == tc.remainingParamsHeight {
|
|
require.NoError(t, err, h)
|
|
require.NotEqual(t, emptyParams, params, h)
|
|
} else {
|
|
require.Error(t, err, h)
|
|
require.Equal(t, emptyParams, params, h)
|
|
}
|
|
|
|
abci, err := stateStore.LoadABCIResponses(h)
|
|
require.Error(t, err, h)
|
|
require.Nil(t, abci, h)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestABCIResponsesResultsHash(t *testing.T) {
|
|
responses := &tmstate.ABCIResponses{
|
|
BeginBlock: &abci.ResponseBeginBlock{},
|
|
DeliverTxs: []*abci.ResponseDeliverTx{
|
|
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
|
|
},
|
|
EndBlock: &abci.ResponseEndBlock{},
|
|
}
|
|
|
|
root := sm.ABCIResponsesResultsHash(responses)
|
|
|
|
// root should be Merkle tree root of DeliverTxs responses
|
|
results := types.NewResults(responses.DeliverTxs)
|
|
assert.Equal(t, root, results.Hash())
|
|
|
|
// test we can prove first DeliverTx
|
|
proof := results.ProveResult(0)
|
|
bz, err := results[0].Marshal()
|
|
require.NoError(t, err)
|
|
assert.NoError(t, proof.Verify(root, bz))
|
|
}
|