mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-07 13:55:17 +00:00
genesis: add support for arbitrary initial height (#5191)
Adds a genesis parameter `initial_height` which specifies the initial block height, as well as ABCI `RequestInitChain.InitialHeight` to pass it to the ABCI application, and `State.InitialHeight` to keep track of the initial height throughout the code. Fixes #2543, based on [RFC-002](https://github.com/tendermint/spec/pull/119). Spec changes in https://github.com/tendermint/spec/pull/135.
This commit is contained in:
@@ -430,7 +430,7 @@ func randState(nValidators int) (*State, []*validatorStub) {
|
||||
return cs, vss
|
||||
}
|
||||
|
||||
func randStateWithEvpool(nValidators int) (*State, []*validatorStub, *evidence.Pool) {
|
||||
func randStateWithEvpool(t *testing.T, nValidators int) (*State, []*validatorStub, *evidence.Pool) {
|
||||
state, privVals := randGenesisState(nValidators, false, 10)
|
||||
|
||||
vss := make([]*validatorStub, nValidators)
|
||||
@@ -451,7 +451,9 @@ func randStateWithEvpool(nValidators int) (*State, []*validatorStub, *evidence.P
|
||||
mempool.EnableTxsAvailable()
|
||||
}
|
||||
stateDB := dbm.NewMemDB()
|
||||
evpool, _ := evidence.NewPool(stateDB, evidenceDB, blockStore)
|
||||
sm.SaveState(stateDB, state)
|
||||
evpool, err := evidence.NewPool(stateDB, evidenceDB, blockStore)
|
||||
require.NoError(t, err)
|
||||
blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyAppConnCon, mempool, evpool)
|
||||
cs := NewState(config.Consensus, state, blockExec, blockStore, mempool, evpool)
|
||||
cs.SetLogger(log.TestingLogger().With("module", "consensus"))
|
||||
@@ -821,9 +823,10 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G
|
||||
sort.Sort(types.PrivValidatorsByAddress(privValidators))
|
||||
|
||||
return &types.GenesisDoc{
|
||||
GenesisTime: tmtime.Now(),
|
||||
ChainID: config.ChainID(),
|
||||
Validators: validators,
|
||||
GenesisTime: tmtime.Now(),
|
||||
InitialHeight: 1,
|
||||
ChainID: config.ChainID(),
|
||||
Validators: validators,
|
||||
}, privValidators
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,14 @@ func (conR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) {
|
||||
case StateChannel:
|
||||
switch msg := msg.(type) {
|
||||
case *NewRoundStepMessage:
|
||||
conR.conS.mtx.Lock()
|
||||
initialHeight := conR.conS.state.InitialHeight
|
||||
conR.conS.mtx.Unlock()
|
||||
if err = msg.ValidateHeight(initialHeight); err != nil {
|
||||
conR.Logger.Error("Peer sent us invalid msg", "peer", src, "msg", msg, "err", err)
|
||||
conR.Switch.StopPeerForError(src, err)
|
||||
return
|
||||
}
|
||||
ps.ApplyNewRoundStepMessage(msg)
|
||||
case *NewValidBlockMessage:
|
||||
ps.ApplyNewValidBlockMessage(msg)
|
||||
@@ -1435,9 +1443,29 @@ func (m *NewRoundStepMessage) ValidateBasic() error {
|
||||
|
||||
// NOTE: SecondsSinceStartTime may be negative
|
||||
|
||||
if (m.Height == 1 && m.LastCommitRound != -1) ||
|
||||
(m.Height > 1 && m.LastCommitRound < 0) {
|
||||
return errors.New("invalid LastCommitRound (for 1st block: -1, for others: >= 0)")
|
||||
// LastCommitRound will be -1 for the initial height, but we don't know what height this is
|
||||
// since it can be specified in genesis. The reactor will have to validate this via
|
||||
// ValidateHeight().
|
||||
if m.LastCommitRound < -1 {
|
||||
return errors.New("invalid LastCommitRound (cannot be < -1)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateHeight validates the height given the chain's initial height.
|
||||
func (m *NewRoundStepMessage) ValidateHeight(initialHeight int64) error {
|
||||
if m.Height < initialHeight {
|
||||
return fmt.Errorf("invalid Height %v (lower than initial height %v)",
|
||||
m.Height, initialHeight)
|
||||
}
|
||||
if m.Height == initialHeight && m.LastCommitRound != -1 {
|
||||
return fmt.Errorf("invalid LastCommitRound %v (must be -1 for initial height %v)",
|
||||
m.LastCommitRound, initialHeight)
|
||||
}
|
||||
if m.Height > initialHeight && m.LastCommitRound < 0 {
|
||||
return fmt.Errorf("LastCommitRound can only be negative for initial height %v", // nolint
|
||||
initialHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -710,8 +710,9 @@ func TestNewRoundStepMessageValidateBasic(t *testing.T) {
|
||||
{true, -1, 0, 0, "Negative round", cstypes.RoundStepNewHeight},
|
||||
{true, 0, 0, -1, "Negative height", cstypes.RoundStepNewHeight},
|
||||
{true, 0, 0, 0, "Invalid Step", cstypes.RoundStepCommit + 1},
|
||||
{true, 0, 0, 1, "H == 1 but LCR != -1 ", cstypes.RoundStepNewHeight},
|
||||
{true, 0, -1, 2, "H > 1 but LCR < 0", cstypes.RoundStepNewHeight},
|
||||
// The following cases will be handled by ValidateHeight
|
||||
{false, 0, 0, 1, "H == 1 but LCR != -1 ", cstypes.RoundStepNewHeight},
|
||||
{false, 0, -1, 2, "H > 1 but LCR < 0", cstypes.RoundStepNewHeight},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -724,7 +725,47 @@ func TestNewRoundStepMessageValidateBasic(t *testing.T) {
|
||||
LastCommitRound: tc.messageLastCommitRound,
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
||||
err := message.ValidateBasic()
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRoundStepMessageValidateHeight(t *testing.T) {
|
||||
initialHeight := int64(10)
|
||||
testCases := []struct { // nolint: maligned
|
||||
expectErr bool
|
||||
messageLastCommitRound int32
|
||||
messageHeight int64
|
||||
testName string
|
||||
}{
|
||||
{false, 0, 11, "Valid Message"},
|
||||
{true, 0, -1, "Negative height"},
|
||||
{true, 0, 0, "Zero height"},
|
||||
{true, 0, 10, "Initial height but LCR != -1 "},
|
||||
{true, -1, 11, "Normal height but LCR < 0"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
message := NewRoundStepMessage{
|
||||
Height: tc.messageHeight,
|
||||
Round: 0,
|
||||
Step: cstypes.RoundStepNewHeight,
|
||||
LastCommitRound: tc.messageLastCommitRound,
|
||||
}
|
||||
|
||||
err := message.ValidateHeight(initialHeight)
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +120,21 @@ func (cs *State) catchupReplay(csHeight int64) error {
|
||||
// Search for last height marker.
|
||||
//
|
||||
// Ignore data corruption errors in previous heights because we only care about last height
|
||||
gr, found, err = cs.wal.SearchForEndHeight(csHeight-1, &WALSearchOptions{IgnoreDataCorruptionErrors: true})
|
||||
if csHeight < cs.state.InitialHeight {
|
||||
return fmt.Errorf("cannot replay height %v, below initial height %v", csHeight, cs.state.InitialHeight)
|
||||
}
|
||||
endHeight := csHeight - 1
|
||||
if csHeight == cs.state.InitialHeight {
|
||||
endHeight = 0
|
||||
}
|
||||
gr, found, err = cs.wal.SearchForEndHeight(endHeight, &WALSearchOptions{IgnoreDataCorruptionErrors: true})
|
||||
if err == io.EOF {
|
||||
cs.Logger.Error("Replay: wal.group.Search returned EOF", "#ENDHEIGHT", csHeight-1)
|
||||
cs.Logger.Error("Replay: wal.group.Search returned EOF", "#ENDHEIGHT", endHeight)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("cannot replay height %d. WAL does not contain #ENDHEIGHT for %d", csHeight, csHeight-1)
|
||||
return fmt.Errorf("cannot replay height %d. WAL does not contain #ENDHEIGHT for %d", csHeight, endHeight)
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
@@ -254,7 +261,7 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error {
|
||||
"protocol-version", res.AppVersion,
|
||||
)
|
||||
|
||||
// Only set the version if we're starting from zero.
|
||||
// Only set the version if there is no existing state.
|
||||
if h.initialState.LastBlockHeight == 0 {
|
||||
h.initialState.Version.Consensus.App = res.AppVersion
|
||||
}
|
||||
@@ -306,6 +313,7 @@ func (h *Handshaker) ReplayBlocks(
|
||||
req := abci.RequestInitChain{
|
||||
Time: h.genDoc.GenesisTime,
|
||||
ChainId: h.genDoc.ChainID,
|
||||
InitialHeight: h.genDoc.InitialHeight,
|
||||
ConsensusParams: csParams,
|
||||
Validators: nextVals,
|
||||
AppStateBytes: h.genDoc.AppState,
|
||||
@@ -353,7 +361,11 @@ func (h *Handshaker) ReplayBlocks(
|
||||
assertAppHashEqualsOneFromState(appHash, state)
|
||||
return appHash, nil
|
||||
|
||||
case appBlockHeight < storeBlockBase-1:
|
||||
case appBlockHeight == 0 && state.InitialHeight < storeBlockBase:
|
||||
// the app has no state, and the block store is truncated above the initial height
|
||||
return appHash, sm.ErrAppBlockHeightTooLow{AppHeight: appBlockHeight, StoreBase: storeBlockBase}
|
||||
|
||||
case appBlockHeight > 0 && appBlockHeight < storeBlockBase-1:
|
||||
// the app is too far behind truncated store (can be 1 behind since we replay the next)
|
||||
return appHash, sm.ErrAppBlockHeightTooLow{AppHeight: appBlockHeight, StoreBase: storeBlockBase}
|
||||
|
||||
@@ -444,7 +456,11 @@ func (h *Handshaker) replayBlocks(
|
||||
if mutateState {
|
||||
finalBlock--
|
||||
}
|
||||
for i := appBlockHeight + 1; i <= finalBlock; i++ {
|
||||
firstBlock := appBlockHeight + 1
|
||||
if firstBlock == 1 {
|
||||
firstBlock = state.InitialHeight
|
||||
}
|
||||
for i := firstBlock; i <= finalBlock; i++ {
|
||||
h.logger.Info("Applying block", "height", i)
|
||||
block := h.store.LoadBlock(i)
|
||||
// Extra check to ensure the app was not changed in a way it shouldn't have.
|
||||
@@ -452,7 +468,7 @@ func (h *Handshaker) replayBlocks(
|
||||
assertAppHashEqualsOneFromBlock(appHash, block)
|
||||
}
|
||||
|
||||
appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateDB)
|
||||
appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateDB, h.genDoc.InitialHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -153,7 +153,8 @@ LOOP:
|
||||
logger := log.NewNopLogger()
|
||||
blockDB := dbm.NewMemDB()
|
||||
stateDB := blockDB
|
||||
state, _ := sm.MakeGenesisStateFromFile(consensusReplayConfig.GenesisFile())
|
||||
state, err := sm.MakeGenesisStateFromFile(consensusReplayConfig.GenesisFile())
|
||||
require.NoError(t, err)
|
||||
privValidator := loadPrivValidator(consensusReplayConfig)
|
||||
cs := newStateWithConfigAndBlockStore(
|
||||
consensusReplayConfig,
|
||||
|
||||
@@ -555,27 +555,33 @@ func (cs *State) updateToState(state sm.State) {
|
||||
panic(fmt.Sprintf("updateToState() expected state height of %v but found %v",
|
||||
cs.Height, state.LastBlockHeight))
|
||||
}
|
||||
if !cs.state.IsEmpty() && cs.state.LastBlockHeight+1 != cs.Height {
|
||||
// This might happen when someone else is mutating cs.state.
|
||||
// Someone forgot to pass in state.Copy() somewhere?!
|
||||
panic(fmt.Sprintf("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v",
|
||||
cs.state.LastBlockHeight+1, cs.Height))
|
||||
}
|
||||
if !cs.state.IsEmpty() {
|
||||
if cs.state.LastBlockHeight > 0 && cs.state.LastBlockHeight+1 != cs.Height {
|
||||
// This might happen when someone else is mutating cs.state.
|
||||
// Someone forgot to pass in state.Copy() somewhere?!
|
||||
panic(fmt.Sprintf("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v",
|
||||
cs.state.LastBlockHeight+1, cs.Height))
|
||||
}
|
||||
if cs.state.LastBlockHeight > 0 && cs.Height == cs.state.InitialHeight {
|
||||
panic(fmt.Sprintf("Inconsistent cs.state.LastBlockHeight %v, expected 0 for initial height %v",
|
||||
cs.state.LastBlockHeight, cs.state.InitialHeight))
|
||||
}
|
||||
|
||||
// If state isn't further out than cs.state, just ignore.
|
||||
// This happens when SwitchToConsensus() is called in the reactor.
|
||||
// We don't want to reset e.g. the Votes, but we still want to
|
||||
// signal the new round step, because other services (eg. txNotifier)
|
||||
// depend on having an up-to-date peer state!
|
||||
if !cs.state.IsEmpty() && (state.LastBlockHeight <= cs.state.LastBlockHeight) {
|
||||
cs.Logger.Info(
|
||||
"Ignoring updateToState()",
|
||||
"newHeight",
|
||||
state.LastBlockHeight+1,
|
||||
"oldHeight",
|
||||
cs.state.LastBlockHeight+1)
|
||||
cs.newStep()
|
||||
return
|
||||
// If state isn't further out than cs.state, just ignore.
|
||||
// This happens when SwitchToConsensus() is called in the reactor.
|
||||
// We don't want to reset e.g. the Votes, but we still want to
|
||||
// signal the new round step, because other services (eg. txNotifier)
|
||||
// depend on having an up-to-date peer state!
|
||||
if state.LastBlockHeight <= cs.state.LastBlockHeight {
|
||||
cs.Logger.Info(
|
||||
"Ignoring updateToState()",
|
||||
"newHeight",
|
||||
state.LastBlockHeight+1,
|
||||
"oldHeight",
|
||||
cs.state.LastBlockHeight+1)
|
||||
cs.newStep()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reset fields based on state.
|
||||
@@ -595,13 +601,16 @@ func (cs *State) updateToState(state sm.State) {
|
||||
case cs.LastCommit == nil:
|
||||
// NOTE: when Tendermint starts, it has no votes. reconstructLastCommit
|
||||
// must be called to reconstruct LastCommit from SeenCommit.
|
||||
panic(fmt.Sprintf("LastCommit cannot be empty in heights > 1 (H:%d)",
|
||||
panic(fmt.Sprintf("LastCommit cannot be empty after initial block (H:%d)",
|
||||
state.LastBlockHeight+1,
|
||||
))
|
||||
}
|
||||
|
||||
// Next desired block height
|
||||
height := state.LastBlockHeight + 1
|
||||
if height == 1 {
|
||||
height = state.InitialHeight
|
||||
}
|
||||
|
||||
// RoundState fields
|
||||
cs.updateHeight(height)
|
||||
@@ -933,7 +942,7 @@ func (cs *State) enterNewRound(height int64, round int32) {
|
||||
// needProofBlock returns true on the first height (so the genesis app hash is signed right away)
|
||||
// and where the last block (height-1) caused the app hash to change
|
||||
func (cs *State) needProofBlock(height int64) bool {
|
||||
if height == 1 {
|
||||
if height == cs.state.InitialHeight {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1090,7 +1099,7 @@ func (cs *State) createProposalBlock() (block *types.Block, blockParts *types.Pa
|
||||
|
||||
var commit *types.Commit
|
||||
switch {
|
||||
case cs.Height == 1:
|
||||
case cs.Height == cs.state.InitialHeight:
|
||||
// We're creating a proposal for the first block.
|
||||
// The commit is empty, but not nil.
|
||||
commit = types.NewCommit(0, 0, types.BlockID{}, nil)
|
||||
@@ -1607,7 +1616,7 @@ func (cs *State) recordMetrics(height int64, block *types.Block) {
|
||||
// height=0 -> MissingValidators and MissingValidatorsPower are both 0.
|
||||
// Remember that the first LastCommit is intentionally empty, so it's not
|
||||
// fair to increment missing validators number.
|
||||
if height > 1 {
|
||||
if height > cs.state.InitialHeight {
|
||||
// Sanity check that commit size matches validator set size - only applies
|
||||
// after first block.
|
||||
var (
|
||||
@@ -1818,7 +1827,7 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) {
|
||||
return added, err
|
||||
}
|
||||
var timestamp time.Time
|
||||
if voteErr.VoteA.Height == 1 {
|
||||
if voteErr.VoteA.Height == cs.state.InitialHeight {
|
||||
timestamp = cs.state.LastBlockTime // genesis time
|
||||
} else {
|
||||
timestamp = sm.MedianTime(cs.LastCommit.MakeCommit(), cs.LastValidators)
|
||||
|
||||
@@ -620,7 +620,7 @@ func TestStateLockPOLRelockThenChangeLock(t *testing.T) {
|
||||
|
||||
// 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka
|
||||
func TestStateLockPOLUnlock(t *testing.T) {
|
||||
cs1, vss, evpool := randStateWithEvpool(4)
|
||||
cs1, vss, evpool := randStateWithEvpool(t, 4)
|
||||
vs2, vs3, vs4 := vss[1], vss[2], vss[3]
|
||||
height, round := cs1.Height, cs1.Round
|
||||
|
||||
@@ -723,7 +723,7 @@ func TestStateLockPOLUnlock(t *testing.T) {
|
||||
// v1 should unlock and precommit nil. In the third round another block is proposed, all vals
|
||||
// prevote and now v1 can lock onto the third block and precommit that
|
||||
func TestStateLockPOLUnlockOnUnknownBlock(t *testing.T) {
|
||||
cs1, vss, evpool := randStateWithEvpool(4)
|
||||
cs1, vss, evpool := randStateWithEvpool(t, 4)
|
||||
vs2, vs3, vs4 := vss[1], vss[2], vss[3]
|
||||
height, round := cs1.Height, cs1.Round
|
||||
|
||||
|
||||
Reference in New Issue
Block a user