From fe26e40af34f7c4b2b853e7c60bbb096a8268494 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Mon, 4 Aug 2025 14:14:06 -0700 Subject: [PATCH 1/4] Skip endblocker flag --- baseapp/baseapp.go | 9 ++++++++ baseapp/options.go | 10 +++++++++ server/start.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 16f84645aa22..6f35218a920e 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -163,6 +163,10 @@ 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 + // 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 @@ -735,6 +739,11 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { 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 { diff --git a/baseapp/options.go b/baseapp/options.go index 1edcdbfe7c9b..c13aeea46bce 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -144,6 +144,11 @@ 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) } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") @@ -409,6 +414,11 @@ 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 +} + // 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..300d94b8e80a 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" + FlagSkipEndBlocker = "skip-endblocker" // 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 skip-endblocker flag is set and configure the app accordingly + if skipEndBlocker := svrCtx.Viper.GetBool(FlagSkipEndBlocker); skipEndBlocker { + baseAppPtr := getBaseAppFromApp(app) + if baseAppPtr != nil { + baseAppPtr.SetSkipEndBlocker(true) + svrCtx.Logger.Info("EndBlocker processing disabled via flag") + } else { + svrCtx.Logger.Warn("Skip EndBlocker 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(FlagSkipEndBlocker, false, "Skip EndBlocker processing for non-blocking query mode") // support old flags name for backwards compatibility cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { From d06bd6f1e38748dd0c0ec9983fc5d53638edab67 Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Tue, 5 Aug 2025 13:15:08 -0700 Subject: [PATCH 2/4] app.deliverTxQueryOnly --- baseapp/abci.go | 6 ++++- baseapp/baseapp.go | 50 +++++++++++++++++++++++++++++++++++++++++ baseapp/baseapp_test.go | 21 +++++++++++++++++ baseapp/options.go | 17 ++++++++++++++ server/start.go | 14 ++++++------ 5 files changed, 100 insertions(+), 8 deletions(-) diff --git a/baseapp/abci.go b/baseapp/abci.go index c1346cf77d85..b56c2d6e57c2 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -818,7 +818,11 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Finaliz var response *abci.ExecTxResult if _, err := app.txDecoder(rawTx); err == nil { - response = app.deliverTx(rawTx) + if app.queryOnlyMode { + response = app.deliverTxQueryOnly(rawTx) + } else { + response = app.deliverTx(rawTx) + } } else { // In the case where a transaction included in a block proposal is malformed, // we still want to return a default response to comet. This is because comet diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 6f35218a920e..6aae90f7f35d 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -167,6 +167,11 @@ type BaseApp struct { // 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 + // 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 @@ -264,6 +269,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 @@ -650,6 +665,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()) @@ -677,6 +696,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 { @@ -734,6 +758,32 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { return resp } +// deliverTxQueryOnly processes a transaction in query-only mode by only updating +// raw state without executing application logic +func (app *BaseApp) deliverTxQueryOnly(tx []byte) *abci.ExecTxResult { + // In query-only mode, we still need to decode transactions to maintain + // state consistency, but we skip the actual execution + if _, err := app.txDecoder(tx); err != nil { + // Return error response for malformed transactions + return sdkerrors.ResponseExecTxResultWithEvents( + sdkerrors.ErrTxDecode, 0, 0, nil, false, + ) + } + + // For query-only mode, return success without executing + // This maintains state consistency without application processing + return &abci.ExecTxResult{ + Code: 0, + Data: nil, + Log: "query-only mode: transaction skipped", + Info: "", + GasWanted: 0, + GasUsed: 0, + Events: nil, + Codespace: "", + } +} + // 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) { 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 c13aeea46bce..9e9573e52869 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -149,6 +149,11 @@ 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) } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") @@ -419,6 +424,18 @@ 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 + } +} + // 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 300d94b8e80a..4b33be1dd0d6 100644 --- a/server/start.go +++ b/server/start.go @@ -81,7 +81,7 @@ const ( FlagDisableIAVLFastNode = "iavl-disable-fastnode" FlagIAVLSyncPruning = "iavl-sync-pruning" FlagShutdownGrace = "shutdown-grace" - FlagSkipEndBlocker = "skip-endblocker" + FlagQueryOnlyMode = "query-only-mode" // state sync-related flags @@ -691,14 +691,14 @@ func startApp(svrCtx *Context, appCreator types.AppCreator, opts StartCmdOptions app = appCreator(svrCtx.Logger, db, traceWriter, svrCtx.Viper) } - // Check if skip-endblocker flag is set and configure the app accordingly - if skipEndBlocker := svrCtx.Viper.GetBool(FlagSkipEndBlocker); skipEndBlocker { + // 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.SetSkipEndBlocker(true) - svrCtx.Logger.Info("EndBlocker processing disabled via flag") + baseAppPtr.SetQueryOnlyMode(true) + svrCtx.Logger.Info("Query-only mode enabled") } else { - svrCtx.Logger.Warn("Skip EndBlocker flag set but unable to access BaseApp") + svrCtx.Logger.Warn("Query-only mode flag set but unable to access BaseApp") } } @@ -1085,7 +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(FlagSkipEndBlocker, false, "Skip EndBlocker processing for non-blocking query mode") + 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 { From 06c48f7f5787ad8ed89dfcd0f22d3725369ed79b Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Tue, 5 Aug 2025 13:46:31 -0700 Subject: [PATCH 3/4] Checkpoint --- baseapp/abci.go | 6 +----- baseapp/baseapp.go | 51 ++++++++++++++++++---------------------------- baseapp/options.go | 13 ++++++++++++ 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/baseapp/abci.go b/baseapp/abci.go index b56c2d6e57c2..c1346cf77d85 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -818,11 +818,7 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Finaliz var response *abci.ExecTxResult if _, err := app.txDecoder(rawTx); err == nil { - if app.queryOnlyMode { - response = app.deliverTxQueryOnly(rawTx) - } else { - response = app.deliverTx(rawTx) - } + response = app.deliverTx(rawTx) } else { // In the case where a transaction included in a block proposal is malformed, // we still want to return a default response to comet. This is because comet diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 6aae90f7f35d..1bcc3fff85cd 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -172,6 +172,10 @@ type BaseApp struct { // 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 @@ -734,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" @@ -758,31 +763,6 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { return resp } -// deliverTxQueryOnly processes a transaction in query-only mode by only updating -// raw state without executing application logic -func (app *BaseApp) deliverTxQueryOnly(tx []byte) *abci.ExecTxResult { - // In query-only mode, we still need to decode transactions to maintain - // state consistency, but we skip the actual execution - if _, err := app.txDecoder(tx); err != nil { - // Return error response for malformed transactions - return sdkerrors.ResponseExecTxResultWithEvents( - sdkerrors.ErrTxDecode, 0, 0, nil, false, - ) - } - - // For query-only mode, return success without executing - // This maintains state consistency without application processing - return &abci.ExecTxResult{ - Code: 0, - Data: nil, - Log: "query-only mode: transaction skipped", - Info: "", - GasWanted: 0, - GasUsed: 0, - Events: nil, - Codespace: "", - } -} // endBlock is an application-defined function that is called after transactions // have been processed in FinalizeBlock. @@ -957,12 +937,21 @@ func (app *BaseApp) runTx(mode sdk.ExecMode, txBytes []byte, tx sdk.Tx) (gInfo s // is a branch of a branch. runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes) - // Attempt to execute all messages and only update state if all messages pass - // and we're in DeliverTx. Note, runMsgs will never return a reference to a - // Result if any single message fails or does not have a registered Handler. - msgsV2, err := tx.GetMsgsV2() - if err == nil { - result, err = app.runMsgs(runMsgCtx, msgs, msgsV2, mode) + // Handle bypass transaction processing mode - skip message execution but maintain state + if app.bypassTxProcessing { + // Return success without executing messages but maintain gas info + result = &sdk.Result{ + Log: "bypass mode: message execution skipped", + } + err = nil + } else { + // Attempt to execute all messages and only update state if all messages pass + // and we're in DeliverTx. Note, runMsgs will never return a reference to a + // Result if any single message fails or does not have a registered Handler. + msgsV2, err := tx.GetMsgsV2() + if err == nil { + result, err = app.runMsgs(runMsgCtx, msgs, msgsV2, mode) + } } // Run optional postHandlers (should run regardless of the execution result). diff --git a/baseapp/options.go b/baseapp/options.go index 9e9573e52869..e430cb3db1b1 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -154,6 +154,11 @@ 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") @@ -436,6 +441,14 @@ func (app *BaseApp) SetQueryOnlyMode(queryOnlyMode bool) { } } +// 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 From bad115dd338a70cce45f0983b8583af4aacb10bd Mon Sep 17 00:00:00 2001 From: Daniel Olshansky Date: Tue, 5 Aug 2025 14:01:57 -0700 Subject: [PATCH 4/4] Checkpoint --- baseapp/baseapp.go | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 1bcc3fff85cd..f3c4febf7862 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -937,21 +937,12 @@ func (app *BaseApp) runTx(mode sdk.ExecMode, txBytes []byte, tx sdk.Tx) (gInfo s // is a branch of a branch. runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes) - // Handle bypass transaction processing mode - skip message execution but maintain state - if app.bypassTxProcessing { - // Return success without executing messages but maintain gas info - result = &sdk.Result{ - Log: "bypass mode: message execution skipped", - } - err = nil - } else { - // Attempt to execute all messages and only update state if all messages pass - // and we're in DeliverTx. Note, runMsgs will never return a reference to a - // Result if any single message fails or does not have a registered Handler. - msgsV2, err := tx.GetMsgsV2() - if err == nil { - result, err = app.runMsgs(runMsgCtx, msgs, msgsV2, mode) - } + // Attempt to execute all messages and only update state if all messages pass + // and we're in DeliverTx. Note, runMsgs will never return a reference to a + // Result if any single message fails or does not have a registered Handler. + msgsV2, err := tx.GetMsgsV2() + if err == nil { + result, err = app.runMsgs(runMsgCtx, msgs, msgsV2, mode) } // Run optional postHandlers (should run regardless of the execution result). @@ -1017,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