Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add preFinalizeBlockHook to allow VE persistence #16898

Merged
merged 11 commits into from
Jul 14, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* (baseapp) [#16898](https://github.com/cosmos/cosmos-sdk/pull/16898) Add `preFinalizeBlockHook` to allow vote extensions persistence.

### Improvements

* (types) [#16890](https://github.com/cosmos/cosmos-sdk/pull/16890) Remove `GetTxCmd() *cobra.Command` and `GetQueryCmd() *cobra.Command` from `module.AppModuleBasic` interface.
Expand Down
58 changes: 28 additions & 30 deletions baseapp/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ func (app *BaseApp) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitCha
// On a new chain, we consider the init chain block height as 0, even though
// req.InitialHeight is 1 by default.
initHeader := cmtproto.Header{ChainID: req.ChainId, Time: req.Time}
app.initialHeight = req.InitialHeight

app.logger.Info("InitChain", "initialHeight", req.InitialHeight, "chainID", req.ChainId)

// Set the initial height, which will be used to determine if we are proposing
// or processing the first block or not.
app.initialHeight = req.InitialHeight
if app.initialHeight == 0 { // If initial height is 0, set it to 1
app.initialHeight = 1
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bringing this from the PR that was reverted in order to make sure that PrepareProposal/ProcessProposal get the right context on the first block.


// if req.InitialHeight is > 1, then we set the initial version on all stores
if req.InitialHeight > 1 {
Expand Down Expand Up @@ -675,53 +676,50 @@ func (app *BaseApp) FinalizeBlock(req *abci.RequestFinalizeBlock) (*abci.Respons
AppHash: app.LastCommitID().Hash,
}

// Initialize the FinalizeBlock state. If this is the first block, it should
// already be initialized in InitChain. Otherwise app.finalizeBlockState will be
// nil, since it is reset on Commit.
// finalizeBlockState should be set on InitChain or ProcessProposal. If it is
// nil, it means we are replaying this block and we need to set the state here
// given that during block replay ProcessProposal is not executed by CometBFT.
if app.finalizeBlockState == nil {
app.setState(execModeFinalize, header)
} else {
// In the first block, app.finalizeBlockState.ctx will already be initialized
// by InitChain. Context is now updated with Header information.
app.finalizeBlockState.ctx = app.finalizeBlockState.ctx.
WithBlockHeader(header).
WithBlockHeight(req.Height).
WithHeaderInfo(coreheader.Info{
ChainID: app.chainID,
Height: req.Height,
Time: req.Time,
Hash: req.Hash,
AppHash: app.LastCommitID().Hash,
})
}

gasMeter := app.getBlockGasMeter(app.finalizeBlockState.ctx)

// Context is now updated with Header information.
app.finalizeBlockState.ctx = app.finalizeBlockState.ctx.
WithBlockGasMeter(gasMeter).
WithBlockHeader(header).
WithHeaderHash(req.Hash).
WithConsensusParams(app.GetConsensusParams(app.finalizeBlockState.ctx)).
WithVoteInfos(req.DecidedLastCommit.Votes).
WithExecMode(sdk.ExecModeFinalize).
WithHeaderInfo(coreheader.Info{
ChainID: app.chainID,
Height: req.Height,
Time: req.Time,
Hash: req.Hash,
AppHash: app.LastCommitID().Hash,
}).WithCometInfo(cometInfo{
Misbehavior: req.Misbehavior,
ValidatorsHash: req.NextValidatorsHash,
ProposerAddress: req.ProposerAddress,
LastCommit: req.DecidedLastCommit,
})
}).
WithConsensusParams(app.GetConsensusParams(app.finalizeBlockState.ctx)).
WithVoteInfos(req.DecidedLastCommit.Votes).
WithExecMode(sdk.ExecModeFinalize).
WithCometInfo(cometInfo{
Misbehavior: req.Misbehavior,
ValidatorsHash: req.NextValidatorsHash,
ProposerAddress: req.ProposerAddress,
LastCommit: req.DecidedLastCommit,
})

// GasMeter must be set after we get a context with updated consensus params.
gasMeter := app.getBlockGasMeter(app.finalizeBlockState.ctx)
app.finalizeBlockState.ctx = app.finalizeBlockState.ctx.WithBlockGasMeter(gasMeter)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lines above are to simplify context setting. The stuff done in the else bracket are also done outside of it, so we might as well do it once.


if app.checkState != nil {
app.checkState.ctx = app.checkState.ctx.
WithBlockGasMeter(gasMeter).
WithHeaderHash(req.Hash)
}

if app.preFinalizeBlockHook != nil {
if err := app.preFinalizeBlockHook(app.finalizeBlockState.ctx, req); err != nil {
return nil, err
}
}

beginBlock := app.beginBlock(req)
events = append(events, beginBlock.Events...)

Expand Down
193 changes: 193 additions & 0 deletions baseapp/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package baseapp_test
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -1908,3 +1910,194 @@ func TestABCI_HaltChain(t *testing.T) {
})
}
}

func TestBaseApp_PreFinalizeBlockHook(t *testing.T) {
db := dbm.NewMemDB()
name := t.Name()
logger := log.NewTestLogger(t)

app := baseapp.NewBaseApp(name, logger, db, nil)
_, err := app.InitChain(&abci.RequestInitChain{})
require.NoError(t, err)

wasHookCalled := false
app.SetPreFinalizeBlockHook(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) error {
wasHookCalled = true
return nil
})
app.Seal()

_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
require.NoError(t, err)
require.Equal(t, true, wasHookCalled)

// Now try erroring
app = baseapp.NewBaseApp(name, logger, db, nil)
_, err = app.InitChain(&abci.RequestInitChain{})
require.NoError(t, err)

app.SetPreFinalizeBlockHook(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) error {
return errors.New("some error")
})
app.Seal()

_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
require.Error(t, err)
}

// TestBaseApp_VoteExtensions tests vote extensions using a price as an example.
func TestBaseApp_VoteExtensions(t *testing.T) {
baseappOpts := func(app *baseapp.BaseApp) {
app.SetExtendVoteHandler(func(sdk.Context, *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
// here we would have a process to get the price from an external source
price := 10000000 + rand.Int63n(1000000)
ve := make([]byte, 8)
binary.BigEndian.PutUint64(ve, uint64(price))
return &abci.ResponseExtendVote{VoteExtension: ve}, nil
})

app.SetVerifyVoteExtensionHandler(func(_ sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
vePrice := binary.BigEndian.Uint64(req.VoteExtension)
// here we would do some price validation, must not be 0 and not too high
if vePrice > 11000000 || vePrice == 0 {
// usually application should always return ACCEPT unless they really want to discard the entire vote
return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, nil
}

return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil
})

app.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
txs := [][]byte{}

// add all VE as txs (in a real scenario we would need to check signatures too)
for _, v := range req.LocalLastCommit.Votes {
if len(v.VoteExtension) == 8 {
// pretend this is a way to check if the VE is valid
if binary.BigEndian.Uint64(v.VoteExtension) < 11000000 && binary.BigEndian.Uint64(v.VoteExtension) > 0 {
txs = append(txs, v.VoteExtension)
}
}
}

return &abci.ResponsePrepareProposal{Txs: txs}, nil
})

app.SetProcessProposal(func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
// here we check if the proposal is valid, mainly if the vote extensions appended to the txs are valid
for _, v := range req.Txs {
// pretend this is a way to check if the tx is actually a VE
if len(v) == 8 {
// pretend this is a way to check if the VE is valid
if binary.BigEndian.Uint64(v) > 11000000 || binary.BigEndian.Uint64(v) == 0 {
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
}
}

return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
})

app.SetPreFinalizeBlockHook(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) error {
count := uint64(0)
pricesSum := uint64(0)
for _, v := range req.Txs {
// pretend this is a way to check if the tx is actually a VE
if len(v) == 8 {
count++
pricesSum += binary.BigEndian.Uint64(v)
}
}

if count > 0 {
// we process the average price and store it in the context to make it available for FinalizeBlock
avgPrice := pricesSum / count
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, avgPrice)
ctx.KVStore(capKey1).Set([]byte("avgPrice"), buf)
}

return nil
})
}

suite := NewBaseAppSuite(t, baseappOpts)

_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
ConsensusParams: &cmtproto.ConsensusParams{
Abci: &cmtproto.ABCIParams{
VoteExtensionsEnableHeight: 1,
},
},
})
require.NoError(t, err)

allVEs := [][]byte{}
// simulate getting 10 vote extensions from 10 validators
for i := 0; i < 10; i++ {
ve, err := suite.baseApp.ExtendVote(context.TODO(), &abci.RequestExtendVote{Height: 1})
require.NoError(t, err)
allVEs = append(allVEs, ve.VoteExtension)
}

// add a couple of invalid vote extensions (in what regards to the check we are doing in VerifyVoteExtension/ProcessProposal)
// add a 0 price
ve := make([]byte, 8)
binary.BigEndian.PutUint64(ve, uint64(0))
allVEs = append(allVEs, ve)

// add a price too high
ve = make([]byte, 8)
binary.BigEndian.PutUint64(ve, uint64(13000000))
allVEs = append(allVEs, ve)

// verify all votes, only 10 should be accepted
successful := 0
for _, v := range allVEs {
res, err := suite.baseApp.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{
Height: 1,
VoteExtension: v,
})
require.NoError(t, err)
if res.Status == abci.ResponseVerifyVoteExtension_ACCEPT {
successful++
}
}
require.Equal(t, 10, successful)

prepPropReq := &abci.RequestPrepareProposal{
Height: 1,
LocalLastCommit: abci.ExtendedCommitInfo{
Round: 0,
Votes: []abci.ExtendedVoteInfo{},
},
}

// add all VEs to the local last commit
for _, ve := range allVEs {
prepPropReq.LocalLastCommit.Votes = append(prepPropReq.LocalLastCommit.Votes, abci.ExtendedVoteInfo{VoteExtension: ve})
}

resp, err := suite.baseApp.PrepareProposal(prepPropReq)
require.NoError(t, err)
require.Len(t, resp.Txs, 10)

procPropRes, err := suite.baseApp.ProcessProposal(&abci.RequestProcessProposal{Height: 1, Txs: resp.Txs})
require.NoError(t, err)
require.Equal(t, abci.ResponseProcessProposal_ACCEPT, procPropRes.Status)

_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1, Txs: resp.Txs})
require.NoError(t, err)

// Check if the average price was available in FinalizeBlock's context
avgPrice := getFinalizeBlockStateCtx(suite.baseApp).KVStore(capKey1).Get([]byte("avgPrice"))
require.NotNil(t, avgPrice)
require.GreaterOrEqual(t, binary.BigEndian.Uint64(avgPrice), uint64(10000000))
require.Less(t, binary.BigEndian.Uint64(avgPrice), uint64(11000000))

suite.baseApp.Commit()

// check if avgPrice was committed
committedAvgPrice := suite.baseApp.NewContext(true).KVStore(capKey1).Get([]byte("avgPrice"))
require.Equal(t, avgPrice, committedAvgPrice)
}
31 changes: 10 additions & 21 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,16 @@ type BaseApp struct {
anteHandler sdk.AnteHandler // ante handler for fee and auth
postHandler sdk.PostHandler // post handler, optional, e.g. for tips

initChainer sdk.InitChainer // ABCI InitChain handler
beginBlocker sdk.BeginBlocker // (legacy ABCI) BeginBlock handler
endBlocker sdk.EndBlocker // (legacy ABCI) EndBlock handler
processProposal sdk.ProcessProposalHandler // ABCI ProcessProposal handler
prepareProposal sdk.PrepareProposalHandler // ABCI PrepareProposal
extendVote sdk.ExtendVoteHandler // ABCI ExtendVote handler
verifyVoteExt sdk.VerifyVoteExtensionHandler // ABCI VerifyVoteExtension handler
prepareCheckStater sdk.PrepareCheckStater // logic to run during commit using the checkState
precommiter sdk.Precommiter // logic to run during commit using the deliverState
initChainer sdk.InitChainer // ABCI InitChain handler
beginBlocker sdk.BeginBlocker // (legacy ABCI) BeginBlock handler
endBlocker sdk.EndBlocker // (legacy ABCI) EndBlock handler
processProposal sdk.ProcessProposalHandler // ABCI ProcessProposal handler
prepareProposal sdk.PrepareProposalHandler // ABCI PrepareProposal
extendVote sdk.ExtendVoteHandler // ABCI ExtendVote handler
verifyVoteExt sdk.VerifyVoteExtensionHandler // ABCI VerifyVoteExtension handler
prepareCheckStater sdk.PrepareCheckStater // logic to run during commit using the checkState
precommiter sdk.Precommiter // logic to run during commit using the deliverState
preFinalizeBlockHook sdk.PreFinalizeBlockHook // logic to run before FinalizeBlock

addrPeerFilter sdk.PeerFilter // filter peers by address and port
idPeerFilter sdk.PeerFilter // filter peers by node ID
Expand Down Expand Up @@ -485,18 +486,6 @@ func (app *BaseApp) setState(mode execMode, header cmtproto.Header) {
}
}

// GetFinalizeBlockStateCtx returns the Context associated with the FinalizeBlock
// state. This Context can be used to write data derived from processing vote
// extensions to application state during ProcessProposal.
//
// NOTE:
// - Do NOT use or write to state using this Context unless you intend for
// that state to be committed.
// - Do NOT use or write to state using this Context on the first block.
func (app *BaseApp) GetFinalizeBlockStateCtx() sdk.Context {
return app.finalizeBlockState.ctx
}

// SetCircuitBreaker sets the circuit breaker for the BaseApp.
// The circuit breaker is checked on every message execution to verify if a transaction should be executed or not.
func (app *BaseApp) SetCircuitBreaker(cb CircuitBreaker) {
Expand Down
8 changes: 8 additions & 0 deletions baseapp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ func (app *BaseApp) SetPrecommiter(precommiter sdk.Precommiter) {
app.precommiter = precommiter
}

func (app *BaseApp) SetPreFinalizeBlockHook(hook sdk.PreFinalizeBlockHook) {
if app.sealed {
panic("SetPreFinalizeBlockHook() on sealed BaseApp")
}

app.preFinalizeBlockHook = hook
}

func (app *BaseApp) SetAnteHandler(ah sdk.AnteHandler) {
if app.sealed {
panic("SetAnteHandler() on sealed BaseApp")
Expand Down
Loading