feat: Settable PrepareProposal handler (#13831)

Co-authored-by: Jeancarlo Barrios <JeancarloBarrios@users.noreply.github.com>
This commit is contained in:
Matt Kocubinski 2022-11-11 11:57:53 -06:00 committed by GitHub
parent d725be5915
commit d66b7fa189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 52 deletions

View File

@ -21,7 +21,6 @@ import (
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/mempool"
)
// Supported ABCI Query prefixes
@ -249,44 +248,11 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc
// Ref: https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-060-abci-1.0.md
// Ref: https://github.com/tendermint/tendermint/blob/main/spec/abci/abci%2B%2B_basic_concepts.md
func (app *BaseApp) PrepareProposal(req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
var (
txsBytes [][]byte
byteCount int64
)
ctx := app.getContextForTx(runTxPrepareProposal, []byte{})
iterator := app.mempool.Select(ctx, req.Txs)
for iterator != nil {
memTx := iterator.Tx()
bz, err := app.txEncoder(memTx)
if err != nil {
panic(err)
}
txSize := int64(len(bz))
// NOTE: runTx was already run in CheckTx which calls mempool.Insert so ideally everything in the pool
// should be valid. But some mempool implementations may insert invalid txs, so we check again.
_, _, _, _, err = app.runTx(runTxPrepareProposal, bz)
if err != nil {
err := app.mempool.Remove(memTx)
if err != nil && !errors.Is(err, mempool.ErrTxNotFound) {
panic(err)
}
iterator = iterator.Next()
continue
} else if byteCount += txSize; byteCount <= req.MaxTxBytes {
txsBytes = append(txsBytes, bz)
} else {
break
}
iterator = iterator.Next()
if app.prepareProposal == nil {
panic("PrepareProposal method not set")
}
return abci.ResponsePrepareProposal{Txs: txsBytes}
return app.prepareProposal(ctx, req)
}
// ProcessProposal implements the ProcessProposal ABCI method and returns a

View File

@ -37,6 +37,7 @@ type ABCIv1TestSuite struct {
mempool mempool.Mempool
txConfig client.TxConfig
cdc codec.ProtoCodecMarshaler
options []func(app *baseapp.BaseApp)
}
func TestABCIv1TestSuite(t *testing.T) {
@ -59,7 +60,8 @@ func (s *ABCIv1TestSuite) SetupTest() {
err := depinject.Inject(makeMinimalConfig(), &appBuilder, &cdc, &registry)
require.NoError(t, err)
app := setupBaseApp(t, anteOpt, baseapp.SetMempool(pool))
s.options = append(s.options, baseapp.SetMempool(pool), anteOpt)
app := setupBaseApp(t, s.options...)
baseapptestutil.RegisterInterfaces(registry)
app.SetMsgServiceRouter(baseapp.NewMsgServiceRouter())
app.SetInterfaceRegistry(registry)
@ -121,10 +123,6 @@ func (s *ABCIv1TestSuite) TestABCIv1_HappyPath() {
Txs: reqProposalTxBytes[:],
}
s.baseApp.SetProcessProposal(nil)
require.Panics(t, func() { s.baseApp.ProcessProposal(reqProcessProposal) })
s.baseApp.SetProcessProposal(s.baseApp.DefaultProcessProposal())
resProcessProposal := s.baseApp.ProcessProposal(reqProcessProposal)
require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)

View File

@ -67,6 +67,7 @@ type BaseApp struct { //nolint: maligned
initChainer sdk.InitChainer // initialize state with validators and state blob
beginBlocker sdk.BeginBlocker // logic to run before any txs
processProposal sdk.ProcessProposalHandler // the handler which runs on ABCI ProcessProposal
prepareProposal sdk.PrepareProposalHandler // the handler which runs on ABCI PrepareProposal
endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes
addrPeerFilter sdk.PeerFilter // filter peers by address and port
idPeerFilter sdk.PeerFilter // filter peers by node ID
@ -176,6 +177,10 @@ func NewBaseApp(
app.SetProcessProposal(app.DefaultProcessProposal())
}
if app.prepareProposal == nil {
app.SetPrepareProposal(app.DefaultPrepareProposal())
}
if app.interBlockCache != nil {
app.cms.SetInterBlockCache(app.interBlockCache)
}
@ -848,13 +853,65 @@ func createEvents(msg sdk.Msg) sdk.Events {
return sdk.Events{msgEvent}
}
// DefaultPrepareProposal returns the default implementation for processing an ABCI proposal. The application's
// mempool is enumerated and all valid transactions are added to the proposal. Transactions are valid if they:
//
// 1) Successfully encode to bytes.
// 2) Are valid (i.e. pass runTx, AnteHandler only)
//
// Enumeration is halted once RequestPrepareProposal.MaxBytes of transactions is reached or the mempool is exhausted.
// Note that step (2) is identical to the validation step performed in DefaultProcessProposal. It is very important
// that the same validation logic is used in both steps, and applications must ensure that this is the case in
// non-default handlers.
func (app *BaseApp) DefaultPrepareProposal() sdk.PrepareProposalHandler {
return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
var (
txsBytes [][]byte
byteCount int64
)
iterator := app.mempool.Select(ctx, req.Txs)
for iterator != nil {
memTx := iterator.Tx()
bz, err := app.txEncoder(memTx)
if err != nil {
panic(err)
}
txSize := int64(len(bz))
// NOTE: runTx was already run in CheckTx which calls mempool.Insert so ideally everything in the pool
// should be valid. But some mempool implementations may insert invalid txs, so we check again.
_, _, _, _, err = app.runTx(runTxPrepareProposal, bz)
if err != nil {
err := app.mempool.Remove(memTx)
if err != nil && !errors.Is(err, mempool.ErrTxNotFound) {
panic(err)
}
iterator = iterator.Next()
continue
} else if byteCount += txSize; byteCount <= req.MaxTxBytes {
txsBytes = append(txsBytes, bz)
} else {
break
}
iterator = iterator.Next()
}
return abci.ResponsePrepareProposal{Txs: txsBytes}
}
}
// DefaultProcessProposal returns the default implementation for processing an ABCI proposal.
// Every transaction in the proposal must pass 2 conditions:
//
// 1. The transaction bytes must decode to a valid transaction.
// 2. The transaction must be valid (i.e. pass runTx, AnteHandler only)
//
// If any transaction fails to pass either condition, the proposal is rejected.
// If any transaction fails to pass either condition, the proposal is rejected. Note that step (2) is identical to the
// validation step performed in DefaultPrepareProposal. It is very important that the same validation logic is used
// in both steps, and applications must ensure that this is the case in non-default handlers.
func (app *BaseApp) DefaultProcessProposal() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal {
for _, txBytes := range req.Txs {

View File

@ -86,11 +86,6 @@ func SetMempool(mempool mempool.Mempool) func(*BaseApp) {
return func(app *BaseApp) { app.SetMempool(mempool) }
}
// SetProcessProposal sets the ProcessProposal handler.
func SetProcessProposal(proposalHandler sdk.ProcessProposalHandler) func(*BaseApp) {
return func(app *BaseApp) { app.SetProcessProposal(proposalHandler) }
}
func (app *BaseApp) SetName(name string) {
if app.sealed {
panic("SetName() on sealed BaseApp")
@ -266,9 +261,25 @@ func (app *BaseApp) SetQueryMultiStore(ms sdk.MultiStore) {
// SetMempool sets the mempool for the BaseApp and is required for the app to start up.
func (app *BaseApp) SetMempool(mempool mempool.Mempool) {
if app.sealed {
panic("SetMempool() on sealed BaseApp")
}
app.mempool = mempool
}
// SetProcessProposal sets the process proposal function for the BaseApp.
func (app *BaseApp) SetProcessProposal(handler sdk.ProcessProposalHandler) {
if app.sealed {
panic("SetProcessProposal() on sealed BaseApp")
}
app.processProposal = handler
}
// SetPrepareProposal sets the prepare proposal function for the BaseApp.
func (app *BaseApp) SetPrepareProposal(handler sdk.PrepareProposalHandler) {
if app.sealed {
panic("SetPrepareProposal() on sealed BaseApp")
}
app.prepareProposal = handler
}

View File

@ -14,7 +14,6 @@ import (
dbm "github.com/tendermint/tm-db"
"cosmossdk.io/depinject"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
@ -189,6 +188,21 @@ func NewSimApp(
var (
app = &SimApp{}
appBuilder *runtime.AppBuilder
// Below we could construct and set an application specific mempool and ABCI 1.0 Prepare and Process Proposal
// handlers. These defaults are already set in the SDK's BaseApp, this shows an example of how to override
// them.
//
//nonceMempool = mempool.NewNonceMempool()
//mempoolOpt = baseapp.SetMempool(nonceMempool)
//prepareOpt = func(app *baseapp.BaseApp) {
// app.SetPrepareProposal(app.DefaultPrepareProposal())
//}
//processOpt = func(app *baseapp.BaseApp) {
// app.SetProcessProposal(app.DefaultProcessProposal())
//}
//
// Further down we'd set the options in the AppBuilder like below.
//baseAppOptions = append(baseAppOptions, mempoolOpt, prepareOpt, processOpt)
// merge the AppConfig and other configuration in one config
appConfig = depinject.Configs(
@ -202,7 +216,6 @@ func NewSimApp(
//
// AUTH
//
// For providing a custom function required in auth to generate custom account types
// add it below. By default the auth module uses simulation.RandomGenesisAccounts.
//

View File

@ -1,6 +1,8 @@
package types
import abci "github.com/tendermint/tendermint/abci/types"
import (
abci "github.com/tendermint/tendermint/abci/types"
)
// InitChainer initializes application state at genesis
type InitChainer func(ctx Context, req abci.RequestInitChain) abci.ResponseInitChain
@ -21,4 +23,7 @@ type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBloc
type PeerFilter func(info string) abci.ResponseQuery
// ProcessProposalHandler defines a function type alias for processing a proposer
type ProcessProposalHandler func(ctx Context, proposal abci.RequestProcessProposal) abci.ResponseProcessProposal
type ProcessProposalHandler func(Context, abci.RequestProcessProposal) abci.ResponseProcessProposal
// PrepareProposalHandler defines a function type alias for preparing a proposal
type PrepareProposalHandler func(Context, abci.RequestPrepareProposal) abci.ResponsePrepareProposal