@@ -24,6 +24,9 @@ type Manager struct {
2424 // - checkState: Used for CheckTx, which is set based on the previous block's
2525 // state. This state is never committed.
2626 //
27+ // - simulationState: Mirrors the last committed state for simulations. It shares
28+ // the same root snapshot as CheckTx but is never written back.
29+ //
2730 // - prepareProposalState: Used for PrepareProposal, which is set based on the
2831 // previous block's state. This state is never committed. In case of multiple
2932 // consensus rounds, the state is always reset to the previous block's state.
@@ -35,6 +38,7 @@ type Manager struct {
3538 // - finalizeBlockState: Used for FinalizeBlock, which is set based on the
3639 // previous block's state. This state is committed.
3740 checkState * State
41+ simulationState * State
3842 prepareProposalState * State
3943 processProposalState * State
4044 finalizeBlockState * State
@@ -63,6 +67,15 @@ func (mgr *Manager) GetState(mode sdk.ExecMode) *State {
6367 case sdk .ExecModeProcessProposal :
6468 return mgr .processProposalState
6569
70+ case sdk .ExecModeSimulate :
71+ // Keep the simulation context aligned with the CheckTx context while
72+ // preserving its own store branch.
73+ if mgr .checkState != nil && mgr .simulationState != nil {
74+ mgr .simulationState .SetContext (mgr .checkState .Context ().WithMultiStore (mgr .simulationState .MultiStore ))
75+ }
76+
77+ return mgr .simulationState
78+
6679 default :
6780 return mgr .checkState
6881 }
@@ -79,6 +92,20 @@ func (mgr *Manager) SetState(
7992 streamingManager storetypes.StreamingManager ,
8093) {
8194 ms := unbranchedStore .CacheMultiStore ()
95+ if mode == sdk .ExecModeCheck {
96+ // Load the last committed version so CheckTx (and by extension simulations)
97+ // operate on the same state that DeliverTx committed in the previous block.
98+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/20685
99+ //
100+ // Using the versioned cache also unwraps any inter-block cache layers,
101+ // preventing simulation runs from polluting the global inter-block cache
102+ // with transient writes.
103+ // Ref: https://github.com/cosmos/cosmos-sdk/issues/23891
104+ if versionedCache , err := unbranchedStore .CacheMultiStoreWithVersion (h .Height ); err == nil {
105+ ms = versionedCache
106+ }
107+ }
108+
82109 headerInfo := header.Info {
83110 Height : h .Height ,
84111 Time : h .Time ,
@@ -97,8 +124,14 @@ func (mgr *Manager) SetState(
97124
98125 switch mode {
99126 case sdk .ExecModeCheck :
100- baseState .SetContext (baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (mgr .gasConfig .MinGasPrices ))
101- mgr .checkState = baseState
127+ // Simulations never persist state, so they can reuse the base snapshot
128+ // that was branched off the last committed height.
129+ mgr .simulationState = baseState
130+
131+ // Branch again for CheckTx so AnteHandler writes do not leak back into
132+ // the shared simulation snapshot.
133+ checkMs := ms .CacheMultiStore ()
134+ mgr .checkState = NewState (baseState .Context ().WithIsCheckTx (true ).WithMinGasPrices (mgr .gasConfig .MinGasPrices ).WithMultiStore (checkMs ), checkMs )
102135
103136 case sdk .ExecModePrepareProposal :
104137 mgr .prepareProposalState = baseState
@@ -121,6 +154,7 @@ func (mgr *Manager) ClearState(mode sdk.ExecMode) {
121154 switch mode {
122155 case sdk .ExecModeCheck :
123156 mgr .checkState = nil
157+ mgr .simulationState = nil
124158
125159 case sdk .ExecModePrepareProposal :
126160 mgr .prepareProposalState = nil
0 commit comments