diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 16f84645aa22..f3c4febf7862 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -163,6 +163,19 @@ type BaseApp struct { // SAFETY: it's safe to do if validators validate the total gas wanted in the `ProcessProposal`, which is the case in the default handler. disableBlockGasMeter bool + // skipEndBlocker will skip EndBlocker processing when true, useful for query-only modes + // where EndBlocker operations might block or are unnecessary. + skipEndBlocker bool + + // queryOnlyMode will skip all application processing (PreBlocker, BeginBlocker, + // transaction execution, EndBlocker) while still accepting state updates via state sync. + // This enables fast query-only nodes that stay synchronized without executing business logic. + queryOnlyMode bool + + // bypassTxProcessing will skip transaction processing (ante handler, message execution) + // but still maintain transaction decoding and basic validation for state consistency. + bypassTxProcessing bool + // nextBlockDelay is the delay to wait until the next block after ABCI has committed. // This gives the application more time to receive precommits. This is the same as TimeoutCommit, // but can now be set from the application. This value defaults to 0, and CometBFT will use the @@ -260,6 +273,16 @@ func (app *BaseApp) Logger() log.Logger { return app.logger } +// QueryOnlyMode returns whether the BaseApp is in query-only mode. +func (app *BaseApp) QueryOnlyMode() bool { + return app.queryOnlyMode +} + +// SkipEndBlocker returns whether EndBlocker processing is skipped. +func (app *BaseApp) SkipEndBlocker() bool { + return app.skipEndBlocker +} + // Trace returns the boolean value for logging error stack traces. func (app *BaseApp) Trace() bool { return app.trace @@ -646,6 +669,10 @@ func (app *BaseApp) cacheTxContext(ctx sdk.Context, txBytes []byte) (sdk.Context func (app *BaseApp) preBlock(req *abci.FinalizeBlockRequest) ([]abci.Event, error) { var events []abci.Event + if app.queryOnlyMode { + // Skip PreBlocker processing in query-only mode + return events, nil + } if app.abciHandlers.PreBlocker != nil { finalizeState := app.stateManager.GetState(execModeFinalize) ctx := finalizeState.Context().WithEventManager(sdk.NewEventManager()) @@ -673,6 +700,11 @@ func (app *BaseApp) beginBlock(_ *abci.FinalizeBlockRequest) (sdk.BeginBlock, er err error ) + if app.queryOnlyMode { + // Skip BeginBlocker processing in query-only mode + return resp, nil + } + if app.abciHandlers.BeginBlocker != nil { resp, err = app.abciHandlers.BeginBlocker(app.stateManager.GetState(execModeFinalize).Context()) if err != nil { @@ -706,6 +738,7 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted") }() + gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx, nil) if err != nil { resultStr = "failed" @@ -730,11 +763,17 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { return resp } + // endBlock is an application-defined function that is called after transactions // have been processed in FinalizeBlock. func (app *BaseApp) endBlock(_ context.Context) (sdk.EndBlock, error) { var endblock sdk.EndBlock + if app.skipEndBlocker { + // Skip EndBlocker processing when flag is set + return endblock, nil + } + if app.abciHandlers.EndBlocker != nil { eb, err := app.abciHandlers.EndBlocker(app.stateManager.GetState(execModeFinalize).Context()) if err != nil { @@ -969,10 +1008,22 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, msgsV2 []protov2.Me return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "no message handler found for %T", msg) } - // ADR 031 request type routing - msgResult, err := handler(ctx, msg) - if err != nil { - return nil, errorsmod.Wrapf(err, "failed to execute message; message index: %d", i) + var msgResult *sdk.Result + var err error + + // Handle bypass transaction processing mode - skip message execution + if app.bypassTxProcessing { + // Create a minimal successful result without executing the handler + msgResult = &sdk.Result{ + Log: "bypass mode: message handler skipped", + } + err = nil + } else { + // ADR 031 request type routing + msgResult, err = handler(ctx, msg) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to execute message; message index: %d", i) + } } // create message events diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index f03fcc7e3820..a9cb641b27f4 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -1044,3 +1044,24 @@ func TestLoadVersionPruning(t *testing.T) { require.Nil(t, err) testLoadVersionHelper(t, app, int64(7), lastCommitID) } + +func TestQueryOnlyMode(t *testing.T) { + pruningOpt := baseapp.SetPruning(pruningtypes.NewPruningOptions(pruningtypes.PruningDefault)) + db := dbm.NewMemDB() + name := t.Name() + app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil, pruningOpt) + + // Test that query-only mode is initially disabled + require.False(t, app.QueryOnlyMode()) + + // Enable query-only mode + app.SetQueryOnlyMode(true) + require.True(t, app.QueryOnlyMode()) + + // Verify that setting query-only mode also enables skip EndBlocker + require.True(t, app.SkipEndBlocker()) + + // Test that we can disable query-only mode + app.SetQueryOnlyMode(false) + require.False(t, app.QueryOnlyMode()) +} diff --git a/baseapp/options.go b/baseapp/options.go index 1edcdbfe7c9b..e430cb3db1b1 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -144,6 +144,21 @@ func DisableBlockGasMeter() func(*BaseApp) { return func(app *BaseApp) { app.SetDisableBlockGasMeter(true) } } +// SkipEndBlocker skips EndBlocker processing for non-blocking query mode. +func SkipEndBlocker() func(*BaseApp) { + return func(app *BaseApp) { app.SetSkipEndBlocker(true) } +} + +// SetQueryOnlyMode enables comprehensive query-only mode for fast query nodes. +func SetQueryOnlyMode() func(*BaseApp) { + return func(app *BaseApp) { app.SetQueryOnlyMode(true) } +} + +// SetBypassTxProcessing enables bypassing transaction processing while maintaining decoding and validation. +func SetBypassTxProcessing() func(*BaseApp) { + return func(app *BaseApp) { app.SetBypassTxProcessing(true) } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") @@ -409,6 +424,31 @@ func (app *BaseApp) SetDisableBlockGasMeter(disableBlockGasMeter bool) { app.disableBlockGasMeter = disableBlockGasMeter } +// SetSkipEndBlocker sets the skipEndBlocker flag for the BaseApp. +func (app *BaseApp) SetSkipEndBlocker(skipEndBlocker bool) { + app.skipEndBlocker = skipEndBlocker +} + +// SetQueryOnlyMode sets the queryOnlyMode flag for the BaseApp. +func (app *BaseApp) SetQueryOnlyMode(queryOnlyMode bool) { + if app.sealed { + panic("SetQueryOnlyMode() on sealed BaseApp") + } + app.queryOnlyMode = queryOnlyMode + if queryOnlyMode { + // Query-only mode implies skipping EndBlocker as well + app.skipEndBlocker = true + } +} + +// SetBypassTxProcessing sets the bypassTxProcessing flag for the BaseApp. +func (app *BaseApp) SetBypassTxProcessing(bypassTxProcessing bool) { + if app.sealed { + panic("SetBypassTxProcessing() on sealed BaseApp") + } + app.bypassTxProcessing = bypassTxProcessing +} + // SetMsgServiceRouter sets the MsgServiceRouter of a BaseApp. func (app *BaseApp) SetMsgServiceRouter(msgServiceRouter *MsgServiceRouter) { app.msgServiceRouter = msgServiceRouter diff --git a/server/start.go b/server/start.go index 6fe6be010d15..4b33be1dd0d6 100644 --- a/server/start.go +++ b/server/start.go @@ -10,6 +10,7 @@ import ( "net" "os" "path/filepath" + "reflect" "runtime/pprof" "strings" "time" @@ -39,6 +40,7 @@ import ( pruningtypes "cosmossdk.io/store/pruning/types" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" @@ -79,6 +81,7 @@ const ( FlagDisableIAVLFastNode = "iavl-disable-fastnode" FlagIAVLSyncPruning = "iavl-sync-pruning" FlagShutdownGrace = "shutdown-grace" + FlagQueryOnlyMode = "query-only-mode" // state sync-related flags @@ -631,6 +634,42 @@ func getCtx(svrCtx *Context, block bool) (*errgroup.Group, context.Context) { return g, ctx } +// getBaseAppFromApp attempts to extract a BaseApp pointer from various app types +func getBaseAppFromApp(app types.Application) *baseapp.BaseApp { + // Direct cast won't work since Application is an interface that BaseApp doesn't fully implement + // BaseApp doesn't implement RegisterAPIRoutes and other methods required by Application interface + + // Try interface method + if appWithBaseApp, ok := app.(interface{ GetBaseApp() *baseapp.BaseApp }); ok { + return appWithBaseApp.GetBaseApp() + } + + // Use reflection to find embedded BaseApp + appValue := reflect.ValueOf(app) + if appValue.Kind() == reflect.Ptr { + appValue = appValue.Elem() + } + + if appValue.Kind() == reflect.Struct { + // Look for embedded BaseApp field + for i := 0; i < appValue.NumField(); i++ { + field := appValue.Field(i) + fieldType := appValue.Type().Field(i) + + // Check if it's an embedded BaseApp + if fieldType.Type == reflect.TypeOf((*baseapp.BaseApp)(nil)) && fieldType.Anonymous { + if field.CanInterface() { + if baseApp, ok := field.Interface().(*baseapp.BaseApp); ok { + return baseApp + } + } + } + } + } + + return nil +} + func startApp(svrCtx *Context, appCreator types.AppCreator, opts StartCmdOptions) (app types.Application, cleanupFn func(), err error) { traceWriter, traceCleanupFn, err := setupTraceWriter(svrCtx) if err != nil { @@ -652,6 +691,17 @@ func startApp(svrCtx *Context, appCreator types.AppCreator, opts StartCmdOptions app = appCreator(svrCtx.Logger, db, traceWriter, svrCtx.Viper) } + // Check if query-only mode flag is set and configure the app accordingly + if queryOnlyMode := svrCtx.Viper.GetBool(FlagQueryOnlyMode); queryOnlyMode { + baseAppPtr := getBaseAppFromApp(app) + if baseAppPtr != nil { + baseAppPtr.SetQueryOnlyMode(true) + svrCtx.Logger.Info("Query-only mode enabled") + } else { + svrCtx.Logger.Warn("Query-only mode flag set but unable to access BaseApp") + } + } + cleanupFn = func() { traceCleanupFn() if localErr := app.Close(); localErr != nil { @@ -1035,6 +1085,7 @@ func addStartNodeFlags(cmd *cobra.Command, opts StartCmdOptions) { cmd.Flags().Bool(FlagDisableIAVLFastNode, false, "Disable fast node for IAVL tree") cmd.Flags().Int(FlagMempoolMaxTxs, mempool.DefaultMaxTx, "Sets MaxTx value for the app-side mempool") cmd.Flags().Duration(FlagShutdownGrace, 0*time.Second, "On Shutdown, duration to wait for resource clean up") + cmd.Flags().Bool(FlagQueryOnlyMode, false, "Run in query-only mode: accept state via sync but skip application processing") // support old flags name for backwards compatibility cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {