Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions baseapp/baseapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
40 changes: 40 additions & 0 deletions baseapp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Comment on lines +427 to +430
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add sealed check for consistency and safety.

The method should include a sealed check like other setter methods in this file to prevent modification after BaseApp initialization.

Apply this diff to add the sealed check:

 // SetSkipEndBlocker sets the skipEndBlocker flag for the BaseApp.
 func (app *BaseApp) SetSkipEndBlocker(skipEndBlocker bool) {
+	if app.sealed {
+		panic("SetSkipEndBlocker() on sealed BaseApp")
+	}
 	app.skipEndBlocker = skipEndBlocker
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// SetSkipEndBlocker sets the skipEndBlocker flag for the BaseApp.
func (app *BaseApp) SetSkipEndBlocker(skipEndBlocker bool) {
app.skipEndBlocker = skipEndBlocker
}
// SetSkipEndBlocker sets the skipEndBlocker flag for the BaseApp.
func (app *BaseApp) SetSkipEndBlocker(skipEndBlocker bool) {
if app.sealed {
panic("SetSkipEndBlocker() on sealed BaseApp")
}
app.skipEndBlocker = skipEndBlocker
}
🤖 Prompt for AI Agents
In baseapp/options.go around lines 417 to 420, the SetSkipEndBlocker method
lacks a sealed check to prevent modifications after BaseApp initialization. Add
a check at the start of the method to verify if the BaseApp is sealed, and if
so, panic or return an error to block further changes. This ensures consistency
and safety like other setter methods in the file.


// 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
Expand Down
51 changes: 51 additions & 0 deletions server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net"
"os"
"path/filepath"
"reflect"
"runtime/pprof"
"strings"
"time"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -79,6 +81,7 @@ const (
FlagDisableIAVLFastNode = "iavl-disable-fastnode"
FlagIAVLSyncPruning = "iavl-sync-pruning"
FlagShutdownGrace = "shutdown-grace"
FlagQueryOnlyMode = "query-only-mode"

// state sync-related flags

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading