mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-07 13:55:17 +00:00
abci++: add consensus parameter logic to control vote extension require height (#8547)
This PR makes vote extensions optional within Tendermint. A new ConsensusParams field, called ABCIParams.VoteExtensionsEnableHeight, has been added to toggle whether or not extensions should be enabled or disabled depending on the current height of the consensus engine. Related to: #8453
This commit is contained in:
@@ -3,6 +3,7 @@ package state
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -100,15 +101,14 @@ func (blockExec *BlockExecutor) CreateProposalBlock(
|
||||
maxDataBytes := types.MaxDataBytes(maxBytes, evSize, state.Validators.Size())
|
||||
|
||||
txs := blockExec.mempool.ReapMaxBytesMaxGas(maxDataBytes, maxGas)
|
||||
commit := lastExtCommit.StripExtensions()
|
||||
commit := lastExtCommit.ToCommit()
|
||||
block := state.MakeBlock(height, txs, commit, evidence, proposerAddr)
|
||||
|
||||
rpp, err := blockExec.appClient.PrepareProposal(
|
||||
ctx,
|
||||
&abci.RequestPrepareProposal{
|
||||
MaxTxBytes: maxDataBytes,
|
||||
Txs: block.Txs.ToSliceOfBytes(),
|
||||
LocalLastCommit: buildExtendedCommitInfo(lastExtCommit, blockExec.store, state.InitialHeight),
|
||||
LocalLastCommit: buildExtendedCommitInfo(lastExtCommit, blockExec.store, state.InitialHeight, state.ConsensusParams.ABCI),
|
||||
ByzantineValidators: block.Evidence.ToABCI(),
|
||||
Height: block.Height,
|
||||
Time: block.Time,
|
||||
@@ -321,7 +321,7 @@ func (blockExec *BlockExecutor) VerifyVoteExtension(ctx context.Context, vote *t
|
||||
}
|
||||
|
||||
if !resp.IsOK() {
|
||||
return types.ErrVoteInvalidExtension
|
||||
return errors.New("invalid vote extension")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -428,7 +428,7 @@ func buildLastCommitInfo(block *types.Block, store Store, initialHeight int64) a
|
||||
// data, it returns an empty record.
|
||||
//
|
||||
// Assumes that the commit signatures are sorted according to validator index.
|
||||
func buildExtendedCommitInfo(ec *types.ExtendedCommit, store Store, initialHeight int64) abci.ExtendedCommitInfo {
|
||||
func buildExtendedCommitInfo(ec *types.ExtendedCommit, store Store, initialHeight int64, ap types.ABCIParams) abci.ExtendedCommitInfo {
|
||||
if ec.Height < initialHeight {
|
||||
// There are no extended commits for heights below the initial height.
|
||||
return abci.ExtendedCommitInfo{}
|
||||
@@ -466,9 +466,15 @@ func buildExtendedCommitInfo(ec *types.ExtendedCommit, store Store, initialHeigh
|
||||
}
|
||||
|
||||
var ext []byte
|
||||
if ecs.BlockIDFlag == types.BlockIDFlagCommit {
|
||||
// We only care about vote extensions if a validator has voted to
|
||||
// commit.
|
||||
// Check if vote extensions were enabled during the commit's height: ec.Height.
|
||||
// ec is the commit from the previous height, so if extensions were enabled
|
||||
// during that height, we ensure they are present and deliver the data to
|
||||
// the proposer. If they were not enabled during this previous height, we
|
||||
// will not deliver extension data.
|
||||
if ap.VoteExtensionsEnabled(ec.Height) && ecs.BlockIDFlag == types.BlockIDFlagCommit {
|
||||
if err := ecs.EnsureExtension(); err != nil {
|
||||
panic(fmt.Errorf("commit at height %d received with missing vote extensions data", ec.Height))
|
||||
}
|
||||
ext = ecs.Extension
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestFinalizeBlockDecidedLastCommit(t *testing.T) {
|
||||
}
|
||||
|
||||
// block for height 2
|
||||
block := sf.MakeBlock(state, 2, lastCommit.StripExtensions())
|
||||
block := sf.MakeBlock(state, 2, lastCommit.ToCommit())
|
||||
bps, err := block.MakePartSet(testPartSize)
|
||||
require.NoError(t, err)
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
|
||||
@@ -1004,6 +1004,116 @@ func TestPrepareProposalErrorOnPrepareProposalError(t *testing.T) {
|
||||
mp.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestCreateProposalBlockPanicOnAbsentVoteExtensions ensures that the CreateProposalBlock
|
||||
// call correctly panics when the vote extension data is missing from the extended commit
|
||||
// data that the method receives.
|
||||
func TestCreateProposalAbsentVoteExtensions(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
|
||||
// The height that is about to be proposed
|
||||
height int64
|
||||
|
||||
// The first height during which vote extensions will be required for consensus to proceed.
|
||||
extensionEnableHeight int64
|
||||
expectPanic bool
|
||||
}{
|
||||
{
|
||||
name: "missing extension data on first required height",
|
||||
height: 2,
|
||||
extensionEnableHeight: 1,
|
||||
expectPanic: true,
|
||||
},
|
||||
{
|
||||
name: "missing extension during before required height",
|
||||
height: 2,
|
||||
extensionEnableHeight: 2,
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "missing extension data and not required",
|
||||
height: 2,
|
||||
extensionEnableHeight: 0,
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "missing extension data and required in two heights",
|
||||
height: 2,
|
||||
extensionEnableHeight: 3,
|
||||
expectPanic: false,
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
eventBus := eventbus.NewDefault(logger)
|
||||
require.NoError(t, eventBus.Start(ctx))
|
||||
|
||||
app := abcimocks.NewApplication(t)
|
||||
if !testCase.expectPanic {
|
||||
app.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{}, nil)
|
||||
}
|
||||
cc := abciclient.NewLocalClient(logger, app)
|
||||
proxyApp := proxy.New(cc, logger, proxy.NopMetrics())
|
||||
err := proxyApp.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
state, stateDB, privVals := makeState(t, 1, int(testCase.height-1))
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
state.ConsensusParams.ABCI.VoteExtensionsEnableHeight = testCase.extensionEnableHeight
|
||||
mp := &mpmocks.Mempool{}
|
||||
mp.On("Lock").Return()
|
||||
mp.On("Unlock").Return()
|
||||
mp.On("FlushAppConn", mock.Anything).Return(nil)
|
||||
mp.On("Update",
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything).Return(nil)
|
||||
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(types.Txs{})
|
||||
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
logger,
|
||||
proxyApp,
|
||||
mp,
|
||||
sm.EmptyEvidencePool{},
|
||||
nil,
|
||||
eventBus,
|
||||
sm.NopMetrics(),
|
||||
)
|
||||
block := sf.MakeBlock(state, testCase.height, new(types.Commit))
|
||||
bps, err := block.MakePartSet(testPartSize)
|
||||
require.NoError(t, err)
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
|
||||
pa, _ := state.Validators.GetByIndex(0)
|
||||
lastCommit, _ := makeValidCommit(ctx, t, testCase.height-1, blockID, state.Validators, privVals)
|
||||
stripSignatures(lastCommit)
|
||||
if testCase.expectPanic {
|
||||
require.Panics(t, func() {
|
||||
blockExec.CreateProposalBlock(ctx, testCase.height, state, lastCommit, pa) //nolint:errcheck
|
||||
})
|
||||
} else {
|
||||
_, err = blockExec.CreateProposalBlock(ctx, testCase.height, state, lastCommit, pa)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stripSignatures(ec *types.ExtendedCommit) {
|
||||
for i, commitSig := range ec.ExtendedSignatures {
|
||||
commitSig.Extension = nil
|
||||
commitSig.ExtensionSignature = nil
|
||||
ec.ExtendedSignatures[i] = commitSig
|
||||
}
|
||||
}
|
||||
|
||||
func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID {
|
||||
var (
|
||||
h = make([]byte, crypto.HashSize)
|
||||
|
||||
@@ -209,7 +209,12 @@ func (_m *BlockStore) PruneBlocks(height int64) (uint64, error) {
|
||||
}
|
||||
|
||||
// SaveBlock provides a mock function with given fields: block, blockParts, seenCommit
|
||||
func (_m *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.ExtendedCommit) {
|
||||
func (_m *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) {
|
||||
_m.Called(block, blockParts, seenCommit)
|
||||
}
|
||||
|
||||
// SaveBlockWithExtendedCommit provides a mock function with given fields: block, blockParts, seenCommit
|
||||
func (_m *BlockStore) SaveBlockWithExtendedCommit(block *types.Block, blockParts *types.PartSet, seenCommit *types.ExtendedCommit) {
|
||||
_m.Called(block, blockParts, seenCommit)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ type BlockStore interface {
|
||||
LoadBlockMeta(height int64) *types.BlockMeta
|
||||
LoadBlock(height int64) *types.Block
|
||||
|
||||
SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.ExtendedCommit)
|
||||
SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit)
|
||||
SaveBlockWithExtendedCommit(block *types.Block, blockParts *types.PartSet, seenCommit *types.ExtendedCommit)
|
||||
|
||||
PruneBlocks(height int64) (uint64, error)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -59,6 +60,7 @@ func abciResponsesKey(height int64) []byte {
|
||||
|
||||
// stateKey should never change after being set in init()
|
||||
var stateKey []byte
|
||||
var tmpABCIKey []byte
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
@@ -66,6 +68,12 @@ func init() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// temporary extra key before consensus param protos are regenerated
|
||||
// TODO(wbanfield) remove in next PR
|
||||
tmpABCIKey, err = orderedcode.Append(nil, int64(10000))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------
|
||||
@@ -137,6 +145,12 @@ func (store dbStore) loadState(key []byte) (state State, err error) {
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
buf, err = store.db.Get(tmpABCIKey)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
h, _ := binary.Varint(buf)
|
||||
sm.ConsensusParams.ABCI.VoteExtensionsEnableHeight = h
|
||||
|
||||
return *sm, nil
|
||||
}
|
||||
@@ -181,6 +195,11 @@ func (store dbStore) save(state State, key []byte) error {
|
||||
if err := batch.Set(key, stateBz); err != nil {
|
||||
return err
|
||||
}
|
||||
bz := make([]byte, 5)
|
||||
binary.PutVarint(bz, state.ConsensusParams.ABCI.VoteExtensionsEnableHeight)
|
||||
if err := batch.Set(tmpABCIKey, bz); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestValidateBlockHeader(t *testing.T) {
|
||||
*/
|
||||
state, _, lastExtCommit = makeAndCommitGoodBlock(ctx, t,
|
||||
state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil)
|
||||
lastCommit = lastExtCommit.StripExtensions()
|
||||
lastCommit = lastExtCommit.ToCommit()
|
||||
}
|
||||
|
||||
nextHeight := validationTestsStopHeight
|
||||
@@ -234,7 +234,7 @@ func TestValidateBlockCommit(t *testing.T) {
|
||||
privVals,
|
||||
nil,
|
||||
)
|
||||
lastCommit = lastExtCommit.StripExtensions()
|
||||
lastCommit = lastExtCommit.ToCommit()
|
||||
|
||||
/*
|
||||
wrongSigsCommit is fine except for the extra bad precommit
|
||||
@@ -384,7 +384,7 @@ func TestValidateBlockEvidence(t *testing.T) {
|
||||
privVals,
|
||||
evidence,
|
||||
)
|
||||
lastCommit = lastExtCommit.StripExtensions()
|
||||
lastCommit = lastExtCommit.ToCommit()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user