@@ -104,6 +104,9 @@ type BaseApp struct {
104104 // - checkState: Used for CheckTx, which is set based on the previous block's
105105 // state. This state is never committed.
106106 //
107+ // - simulationState: Mirrors the last committed state for simulations. It shares
108+ // the same root snapshot as CheckTx but is never written back.
109+ //
107110 // - prepareProposalState: Used for PrepareProposal, which is set based on the
108111 // previous block's state. This state is never committed. In case of multiple
109112 // consensus rounds, the state is always reset to the previous block's state.
@@ -115,6 +118,7 @@ type BaseApp struct {
115118 // - finalizeBlockState: Used for FinalizeBlock, which is set based on the
116119 // previous block's state. This state is committed.
117120 checkState * state
121+ simulationState * state
118122 prepareProposalState * state
119123 processProposalState * state
120124 finalizeBlockState * state
@@ -478,6 +482,19 @@ func (app *BaseApp) IsSealed() bool { return app.sealed }
478482// multi-store branch, and provided header.
479483func (app * BaseApp ) setState (mode execMode , h cmtproto.Header ) {
480484 ms := app .cms .CacheMultiStore ()
485+ if mode == execModeCheck {
486+ // Load the last committed version so CheckTx (and by extension simulations)
487+ // operate on the same state that DeliverTx committed in the previous block.
488+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/20685
489+ //
490+ // Using the versioned cache also unwraps any inter-block cache layers,
491+ // preventing simulation runs from polluting the global inter-block cache
492+ // with transient writes.
493+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/23891
494+ if versionedCache , err := app .cms .CacheMultiStoreWithVersion (h .Height ); err == nil {
495+ ms = versionedCache
496+ }
497+ }
481498 headerInfo := header.Info {
482499 Height : h .Height ,
483500 Time : h .Time ,
@@ -493,8 +510,14 @@ func (app *BaseApp) setState(mode execMode, h cmtproto.Header) {
493510
494511 switch mode {
495512 case execModeCheck :
496- baseState .SetContext (baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (app .minGasPrices ))
497- app .checkState = baseState
513+ // Simulations never persist state, so they can reuse the base snapshot
514+ // that was branched off the last committed height.
515+ app .simulationState = baseState
516+
517+ // Branch again for CheckTx so AnteHandler writes do not leak back into
518+ // the shared simulation snapshot.
519+ checkMs := ms .CacheMultiStore ()
520+ app .checkState = & state {ctx : baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (app .minGasPrices ).WithMultiStore (checkMs ), ms : checkMs }
498521
499522 case execModePrepareProposal :
500523 app .prepareProposalState = baseState
@@ -640,7 +663,14 @@ func (app *BaseApp) getState(mode execMode) *state {
640663
641664 case execModeProcessProposal :
642665 return app .processProposalState
666+ case execModeSimulate :
667+ // Keep the simulation context aligned with the CheckTx context while
668+ // preserving its own store branch.
669+ if app .checkState != nil && app .simulationState != nil {
670+ app .simulationState .SetContext (app .checkState .Context ().WithMultiStore (app .simulationState .ms ))
671+ }
643672
673+ return app .simulationState
644674 default :
645675 return app .checkState
646676 }
0 commit comments