@@ -109,6 +109,9 @@ type BaseApp struct {
109109 // - checkState: Used for CheckTx, which is set based on the previous block's
110110 // state. This state is never committed.
111111 //
112+ // - simulationState: Mirrors the last committed state for simulations. It shares
113+ // the same root snapshot as CheckTx but is never written back.
114+ //
112115 // - prepareProposalState: Used for PrepareProposal, which is set based on the
113116 // previous block's state. This state is never committed. In case of multiple
114117 // consensus rounds, the state is always reset to the previous block's state.
@@ -120,6 +123,7 @@ type BaseApp struct {
120123 // - finalizeBlockState: Used for FinalizeBlock, which is set based on the
121124 // previous block's state. This state is committed.
122125 checkState * state
126+ simulationState * state
123127 prepareProposalState * state
124128 processProposalState * state
125129 finalizeBlockState * state
@@ -493,6 +497,19 @@ func (app *BaseApp) IsSealed() bool { return app.sealed }
493497// multi-store branch, and provided header.
494498func (app * BaseApp ) setState (mode execMode , h cmtproto.Header ) {
495499 ms := app .cms .CacheMultiStore ()
500+ if mode == execModeCheck {
501+ // Load the last committed version so CheckTx (and by extension simulations)
502+ // operate on the same state that DeliverTx committed in the previous block.
503+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/20685
504+ //
505+ // Using the versioned cache also unwraps any inter-block cache layers,
506+ // preventing simulation runs from polluting the global inter-block cache
507+ // with transient writes.
508+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/23891
509+ if versionedCache , err := app .cms .CacheMultiStoreWithVersion (h .Height ); err == nil {
510+ ms = versionedCache
511+ }
512+ }
496513 headerInfo := header.Info {
497514 Height : h .Height ,
498515 Time : h .Time ,
@@ -508,8 +525,14 @@ func (app *BaseApp) setState(mode execMode, h cmtproto.Header) {
508525
509526 switch mode {
510527 case execModeCheck :
511- baseState .SetContext (baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (app .minGasPrices ))
512- app .checkState = baseState
528+ // Simulations never persist state, so they can reuse the base snapshot
529+ // that was branched off the last committed height.
530+ app .simulationState = baseState
531+
532+ // Branch again for CheckTx so AnteHandler writes do not leak back into
533+ // the shared simulation snapshot.
534+ checkMs := ms .CacheMultiStore ()
535+ app .checkState = & state {ctx : baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (app .minGasPrices ).WithMultiStore (checkMs ), ms : checkMs }
513536
514537 case execModePrepareProposal :
515538 app .prepareProposalState = baseState
@@ -655,7 +678,14 @@ func (app *BaseApp) getState(mode execMode) *state {
655678
656679 case execModeProcessProposal :
657680 return app .processProposalState
681+ case execModeSimulate :
682+ // Keep the simulation context aligned with the CheckTx context while
683+ // preserving its own store branch.
684+ if app .checkState != nil && app .simulationState != nil {
685+ app .simulationState .SetContext (app .checkState .Context ().WithMultiStore (app .simulationState .ms ))
686+ }
658687
688+ return app .simulationState
659689 default :
660690 return app .checkState
661691 }
0 commit comments