Skip to content

Commit

Permalink
feat(server/v2/cometbft): Implement necessary handlers and helpers fo…
Browse files Browse the repository at this point in the history
…r vote extensions (#22830)

(cherry picked from commit 6d12b1d)

# Conflicts:
#	runtime/v2/builder.go
  • Loading branch information
facundomedica authored and mergify[bot] committed Dec 19, 2024
1 parent 243332d commit 2744b05
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 28 deletions.
247 changes: 247 additions & 0 deletions runtime/v2/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package runtime

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"

"cosmossdk.io/core/appmodule"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/store"
"cosmossdk.io/core/transaction"
"cosmossdk.io/runtime/v2/services"

Check failure on line 14 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/runtime/v2/services; to add it:

Check failure on line 14 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/runtime/v2/services; to add it:

Check failure on line 14 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

could not import cosmossdk.io/runtime/v2/services (invalid package name: "")
"cosmossdk.io/server/v2/appmanager"

Check failure on line 15 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/appmanager; to add it:

Check failure on line 15 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/appmanager; to add it:

Check failure on line 15 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

could not import cosmossdk.io/server/v2/appmanager (invalid package name: "")
"cosmossdk.io/server/v2/stf"

Check failure on line 16 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/stf; to add it:

Check failure on line 16 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/stf; to add it:

Check failure on line 16 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

could not import cosmossdk.io/server/v2/stf (invalid package name: "")
"cosmossdk.io/server/v2/stf/branch"

Check failure on line 17 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/stf/branch; to add it:

Check failure on line 17 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/server/v2/stf/branch; to add it:

Check failure on line 17 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

could not import cosmossdk.io/server/v2/stf/branch (invalid package name: "")
"cosmossdk.io/store/v2/root"

Check failure on line 18 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/store/v2/root; to add it:

Check failure on line 18 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

no required module provides package cosmossdk.io/store/v2/root; to add it:

Check failure on line 18 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / dependency-review

could not import cosmossdk.io/store/v2/root (invalid package name: "")
)

// AppBuilder is a type that is injected into a container by the runtime/v2 module
// (as *AppBuilder) which can be used to create an app which is compatible with
// the existing app.go initialization conventions.
type AppBuilder[T transaction.Tx] struct {
app *App[T]

Check failure on line 25 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: App (typecheck)
storeBuilder root.Builder
storeConfig *root.Config

// the following fields are used to overwrite the default
branch func(state store.ReaderMap) store.WriterMap
txValidator func(ctx context.Context, tx T) error
postTxExec func(ctx context.Context, tx T, success bool) error
preblocker func(ctx context.Context, txs []T, mmPreblocker func() error) error
}

// RegisterModules registers the provided modules with the module manager.
// This is the primary hook for integrating with modules which are not registered using the app config.
func (a *AppBuilder[T]) RegisterModules(modules map[string]appmodulev2.AppModule) error {
for name, appModule := range modules {
// if a (legacy) module implements the HasName interface, check that the name matches
if mod, ok := appModule.(interface{ Name() string }); ok {
if name != mod.Name() {
a.app.logger.Warn(fmt.Sprintf("module name %q does not match name returned by HasName: %q", name, mod.Name()))
}
}

if _, ok := a.app.moduleManager.modules[name]; ok {
return fmt.Errorf("module named %q already exists", name)
}
a.app.moduleManager.modules[name] = appModule

if mod, ok := appModule.(appmodulev2.HasRegisterInterfaces); ok {
mod.RegisterInterfaces(a.app.interfaceRegistrar)
}

if mod, ok := appModule.(appmodule.HasAminoCodec); ok {
mod.RegisterLegacyAminoCodec(a.app.amino)
}
}

return nil
}

// Build builds an *App instance.
func (a *AppBuilder[T]) Build(opts ...AppBuilderOption[T]) (*App[T], error) {

Check failure on line 65 in runtime/v2/builder.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: App (typecheck)
for _, opt := range opts {
opt(a)
}

// default branch
if a.branch == nil {
a.branch = branch.DefaultNewWriterMap
}

// default tx validator
if a.txValidator == nil {
a.txValidator = a.app.moduleManager.TxValidators()
}

// default post tx exec
if a.postTxExec == nil {
a.postTxExec = func(ctx context.Context, tx T, success bool) error {
return nil
}
}

var err error
a.app.db, err = a.storeBuilder.Build(a.app.logger, a.storeConfig)
if err != nil {
return nil, err
}

if err = a.app.moduleManager.RegisterServices(a.app); err != nil {
return nil, err
}

endBlocker, valUpdate := a.app.moduleManager.EndBlock()

preblockerFn := func(ctx context.Context, txs []T) error {
if a.preblocker != nil {
return a.preblocker(ctx, txs, func() error {
return a.app.moduleManager.PreBlocker()(ctx, txs)
})
}

// if there is no preblocker set, call the module manager's preblocker directly
return a.app.moduleManager.PreBlocker()(ctx, txs)
}

stf, err := stf.New[T](
a.app.logger.With("module", "stf"),
a.app.msgRouterBuilder,
a.app.queryRouterBuilder,
preblockerFn,
a.app.moduleManager.BeginBlock(),
endBlocker,
a.txValidator,
valUpdate,
a.postTxExec,
a.branch,
)
if err != nil {
return nil, fmt.Errorf("failed to create STF: %w", err)
}
a.app.stf = stf

a.app.AppManager = appmanager.New[T](
appmanager.Config{
ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit,
QueryGasLimit: a.app.config.GasConfig.QueryGasLimit,
SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit,
},
a.app.db,
a.app.stf,
a.initGenesis,
a.exportGenesis,
)

return a.app, nil
}

// initGenesis returns the app initialization genesis for modules
func (a *AppBuilder[T]) initGenesis(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, []appmodule.ValidatorUpdate, error) {
// this implementation assumes that the state is a JSON object
bz, err := io.ReadAll(src)
if err != nil {
return nil, nil, fmt.Errorf("failed to read import state: %w", err)
}

var genesisJSON map[string]json.RawMessage
if err = json.Unmarshal(bz, &genesisJSON); err != nil {
return nil, nil, err
}

v, zeroState, err := a.app.db.StateLatest()
if err != nil {
return nil, nil, fmt.Errorf("unable to get latest state: %w", err)
}
if v != 0 { // TODO: genesis state may be > 0, we need to set version on store
return nil, nil, errors.New("cannot init genesis on non-zero state")
}
genesisCtx := services.NewGenesisContext(a.branch(zeroState))
var valUpdates []appmodulev2.ValidatorUpdate
genesisState, err := genesisCtx.Mutate(ctx, func(ctx context.Context) error {
valUpdates, err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler)
if err != nil {
return fmt.Errorf("failed to init genesis: %w", err)
}

return nil
})

return genesisState, valUpdates, err
}

// exportGenesis returns the app export genesis logic for modules
func (a *AppBuilder[T]) exportGenesis(ctx context.Context, version uint64) ([]byte, error) {
state, err := a.app.db.StateAt(version)
if err != nil {
return nil, fmt.Errorf("unable to get state at given version: %w", err)
}

genesisJson, err := a.app.moduleManager.ExportGenesisForModules(
ctx,
func() store.WriterMap {
return a.branch(state)
},
)
if err != nil {
return nil, fmt.Errorf("failed to export genesis: %w", err)
}

bz, err := json.Marshal(genesisJson)
if err != nil {
return nil, fmt.Errorf("failed to marshal genesis: %w", err)
}

return bz, nil
}

// AppBuilderOption is a function that can be passed to AppBuilder.Build to customize the resulting app.
type AppBuilderOption[T transaction.Tx] func(*AppBuilder[T])

// AppBuilderWithBranch sets a custom branch implementation for the app.
func AppBuilderWithBranch[T transaction.Tx](branch func(state store.ReaderMap) store.WriterMap) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.branch = branch
}
}

// AppBuilderWithTxValidator sets the tx validator for the app.
// It overrides all default tx validators defined by modules.
func AppBuilderWithTxValidator[T transaction.Tx](
txValidators func(
ctx context.Context, tx T,
) error,
) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.txValidator = txValidators
}
}

// AppBuilderWithPostTxExec sets logic that will be executed after each transaction.
// When not provided, a no-op function will be used.
func AppBuilderWithPostTxExec[T transaction.Tx](
postTxExec func(
ctx context.Context, tx T, success bool,
) error,
) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.postTxExec = postTxExec
}
}

// AppBuilderWithPreblocker sets logic that will be executed before each block.
// mmPreblocker can be used to call module manager's preblocker, so that it can be
// called before or after depending on the app's logic.
// This is especially useful when implementing vote extensions.
func AppBuilderWithPreblocker[T transaction.Tx](
preblocker func(
ctx context.Context, txs []T, mmPreblocker func() error,
) error,
) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.preblocker = preblocker
}
}
18 changes: 10 additions & 8 deletions server/v2/cometbft/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ type consensus[T transaction.Tx] struct {

prepareProposalHandler handlers.PrepareHandler[T]
processProposalHandler handlers.ProcessHandler[T]
verifyVoteExt handlers.VerifyVoteExtensionhandler
verifyVoteExt handlers.VerifyVoteExtensionHandler
extendVote handlers.ExtendVoteHandler
checkTxHandler handlers.CheckTxHandler[T]

Expand Down Expand Up @@ -142,7 +142,7 @@ func (c *consensus[T]) Info(ctx context.Context, _ *abciproto.InfoRequest) (*abc
// if height is 0, we dont know the consensus params
var appVersion uint64 = 0
if version > 0 {
cp, err := c.GetConsensusParams(ctx)
cp, err := GetConsensusParams(ctx, c.app)
// if the consensus params are not found, we set the app version to 0
// in the case that the start version is > 0
if cp == nil || errors.Is(err, collections.ErrNotFound) {
Expand Down Expand Up @@ -387,7 +387,7 @@ func (c *consensus[T]) PrepareProposal(
LastCommit: toCoreExtendedCommitInfo(req.LocalLastCommit),
})

txs, err := c.prepareProposalHandler(ciCtx, c.app, c.appCodecs.TxCodec, req)
txs, err := c.prepareProposalHandler(ciCtx, c.app, c.appCodecs.TxCodec, req, c.chainID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -433,7 +433,7 @@ func (c *consensus[T]) ProcessProposal(
LastCommit: toCoreCommitInfo(req.ProposedLastCommit),
})

err := c.processProposalHandler(ciCtx, c.app, c.appCodecs.TxCodec, req)
err := c.processProposalHandler(ciCtx, c.app, c.appCodecs.TxCodec, req, c.chainID)
if err != nil {
c.logger.Error("failed to process proposal", "height", req.Height, "time", req.Time, "hash", fmt.Sprintf("%X", req.Hash), "err", err)
return &abciproto.ProcessProposalResponse{
Expand Down Expand Up @@ -533,7 +533,7 @@ func (c *consensus[T]) FinalizeBlock(

c.lastCommittedHeight.Store(req.Height)

cp, err := c.GetConsensusParams(ctx) // we get the consensus params from the latest state because we committed state above
cp, err := GetConsensusParams(ctx, c.app) // we get the consensus params from the latest state because we committed state above
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -600,7 +600,7 @@ func (c *consensus[T]) Commit(ctx context.Context, _ *abciproto.CommitRequest) (

c.snapshotManager.SnapshotIfApplicable(lastCommittedHeight)

cp, err := c.GetConsensusParams(ctx)
cp, err := GetConsensusParams(ctx, c.app)
if err != nil {
return nil, err
}
Expand All @@ -619,7 +619,7 @@ func (c *consensus[T]) VerifyVoteExtension(
) (*abciproto.VerifyVoteExtensionResponse, error) {
// If vote extensions are not enabled, as a safety precaution, we return an
// error.
cp, err := c.GetConsensusParams(ctx)
cp, err := GetConsensusParams(ctx, c.app)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -658,7 +658,7 @@ func (c *consensus[T]) VerifyVoteExtension(
func (c *consensus[T]) ExtendVote(ctx context.Context, req *abciproto.ExtendVoteRequest) (*abciproto.ExtendVoteResponse, error) {
// If vote extensions are not enabled, as a safety precaution, we return an
// error.
cp, err := c.GetConsensusParams(ctx)
cp, err := GetConsensusParams(ctx, c.app)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -702,6 +702,8 @@ func decodeTxs[T transaction.Tx](logger log.Logger, rawTxs [][]byte, codec trans
if err != nil {
// do not return an error here, as we want to deliver the block even if some txs are invalid
logger.Debug("failed to decode tx", "err", err)
txs[i] = RawTx(rawTx).(T) // allows getting the raw bytes down the line
continue
}
txs[i] = tx
}
Expand Down
10 changes: 5 additions & 5 deletions server/v2/cometbft/handlers/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func NewDefaultProposalHandler[T transaction.Tx](mp mempool.Mempool[T]) *Default
}

func (h *DefaultProposalHandler[T]) PrepareHandler() PrepareHandler[T] {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.PrepareProposalRequest) ([]T, error) {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.PrepareProposalRequest, chainID string) ([]T, error) {
var maxBlockGas uint64

res, err := app.Query(ctx, 0, &consensustypes.QueryParamsRequest{})
Expand Down Expand Up @@ -98,7 +98,7 @@ func (h *DefaultProposalHandler[T]) PrepareHandler() PrepareHandler[T] {
}

func (h *DefaultProposalHandler[T]) ProcessHandler() ProcessHandler[T] {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.ProcessProposalRequest) error {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.ProcessProposalRequest, chainID string) error {
// If the mempool is nil we simply return ACCEPT,
// because PrepareProposal may have included txs that could fail verification.
_, isNoOp := h.mempool.(mempool.NoOpMempool[T])
Expand Down Expand Up @@ -174,15 +174,15 @@ func decodeTxs[T transaction.Tx](codec transaction.Codec[T], txsBz [][]byte) []T
// NoOpPrepareProposal defines a no-op PrepareProposal handler. It will always
// return the transactions sent by the client's request.
func NoOpPrepareProposal[T transaction.Tx]() PrepareHandler[T] {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.PrepareProposalRequest) ([]T, error) {
return func(ctx context.Context, app AppManager[T], codec transaction.Codec[T], req *abci.PrepareProposalRequest, chainID string) ([]T, error) {
return decodeTxs(codec, req.Txs), nil
}
}

// NoOpProcessProposal defines a no-op ProcessProposal Handler. It will always
// return ACCEPT.
func NoOpProcessProposal[T transaction.Tx]() ProcessHandler[T] {
return func(context.Context, AppManager[T], transaction.Codec[T], *abci.ProcessProposalRequest) error {
return func(context.Context, AppManager[T], transaction.Codec[T], *abci.ProcessProposalRequest, string) error {
return nil
}
}
Expand All @@ -197,7 +197,7 @@ func NoOpExtendVote() ExtendVoteHandler {

// NoOpVerifyVoteExtensionHandler defines a no-op VerifyVoteExtension handler. It
// will always return an ACCEPT status with no error.
func NoOpVerifyVoteExtensionHandler() VerifyVoteExtensionhandler {
func NoOpVerifyVoteExtensionHandler() VerifyVoteExtensionHandler {
return func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error) {
return &abci.VerifyVoteExtensionResponse{Status: abci.VERIFY_VOTE_EXTENSION_STATUS_ACCEPT}, nil
}
Expand Down
8 changes: 4 additions & 4 deletions server/v2/cometbft/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import (
type (
// PrepareHandler passes in the list of Txs that are being proposed. The app can then do stateful operations
// over the list of proposed transactions. It can return a modified list of txs to include in the proposal.
PrepareHandler[T transaction.Tx] func(context.Context, AppManager[T], transaction.Codec[T], *abci.PrepareProposalRequest) ([]T, error)
PrepareHandler[T transaction.Tx] func(ctx context.Context, app AppManager[T], cdc transaction.Codec[T], req *abci.PrepareProposalRequest, chainID string) ([]T, error)

// ProcessHandler is a function that takes a list of transactions and returns a boolean and an error.
// If the verification of a transaction fails, the boolean is false and the error is non-nil.
ProcessHandler[T transaction.Tx] func(context.Context, AppManager[T], transaction.Codec[T], *abci.ProcessProposalRequest) error
ProcessHandler[T transaction.Tx] func(ctx context.Context, app AppManager[T], cdc transaction.Codec[T], req *abci.ProcessProposalRequest, chainID string) error

// VerifyVoteExtensionhandler is a function type that handles the verification of a vote extension request.
// VerifyVoteExtensionHandler is a function type that handles the verification of a vote extension request.
// It takes a context, a store reader map, and a request to verify a vote extension.
// It returns a response to verify the vote extension and an error if any.
VerifyVoteExtensionhandler func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error)
VerifyVoteExtensionHandler func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error)

// ExtendVoteHandler is a function type that handles the extension of a vote.
// It takes a context, a store reader map, and a request to extend a vote.
Expand Down
Loading

0 comments on commit 2744b05

Please sign in to comment.