From c49b905f46673b907caec0f2d4eccc1f9485de5a Mon Sep 17 00:00:00 2001 From: beer-1 Date: Fri, 31 Oct 2025 14:52:38 +0900 Subject: [PATCH] fix: isolate CheckTx and simulation state from DeliverTx commits by loading the last committed snapshot and keeping the simulation context in sync --- CHANGELOG.md | 1 + baseapp/baseapp.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb7130477578..13b6e107c48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes * (cli) [#25485](https://github.com/cosmos/cosmos-sdk/pull/25485) Avoid failed to convert address field in `withdraw-validator-commission` cmd. +* (baseapp) [#25531](https://github.com/cosmos/cosmos-sdk/pull/25531) isolate CheckTx and simulation state from DeliverTx commits by loading the last committed snapshot and keeping the simulation context in sync. ## [v0.53.4](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.53.3) - 2025-07-25 diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index c10fa15936a9..d51e0eae23ab 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -109,6 +109,9 @@ type BaseApp struct { // - checkState: Used for CheckTx, which is set based on the previous block's // state. This state is never committed. // + // - simulationState: Mirrors the last committed state for simulations. It shares + // the same root snapshot as CheckTx but is never written back. + // // - prepareProposalState: Used for PrepareProposal, which is set based on the // previous block's state. This state is never committed. In case of multiple // consensus rounds, the state is always reset to the previous block's state. @@ -120,6 +123,7 @@ type BaseApp struct { // - finalizeBlockState: Used for FinalizeBlock, which is set based on the // previous block's state. This state is committed. checkState *state + simulationState *state prepareProposalState *state processProposalState *state finalizeBlockState *state @@ -493,6 +497,19 @@ func (app *BaseApp) IsSealed() bool { return app.sealed } // multi-store branch, and provided header. func (app *BaseApp) setState(mode execMode, h cmtproto.Header) { ms := app.cms.CacheMultiStore() + if mode == execModeCheck { + // Load the last committed version so CheckTx (and by extension simulations) + // operate on the same state that DeliverTx committed in the previous block. + // Ref: https://github.com/cosmos/cosmos-sdk/issues/20685 + // + // Using the versioned cache also unwraps any inter-block cache layers, + // preventing simulation runs from polluting the global inter-block cache + // with transient writes. + // Ref: https://github.com/cosmos/cosmos-sdk/issues/23891 + if versionedCache, err := app.cms.CacheMultiStoreWithVersion(h.Height); err == nil { + ms = versionedCache + } + } headerInfo := header.Info{ Height: h.Height, Time: h.Time, @@ -508,8 +525,14 @@ func (app *BaseApp) setState(mode execMode, h cmtproto.Header) { switch mode { case execModeCheck: - baseState.SetContext(baseState.Context().WithIsCheckTx(true).WithMinGasPrices(app.minGasPrices)) - app.checkState = baseState + // Simulations never persist state, so they can reuse the base snapshot + // that was branched off the last committed height. + app.simulationState = baseState + + // Branch again for CheckTx so AnteHandler writes do not leak back into + // the shared simulation snapshot. + checkMs := ms.CacheMultiStore() + app.checkState = &state{ctx: baseState.Context().WithIsCheckTx(true).WithMinGasPrices(app.minGasPrices).WithMultiStore(checkMs), ms: checkMs} case execModePrepareProposal: app.prepareProposalState = baseState @@ -655,7 +678,14 @@ func (app *BaseApp) getState(mode execMode) *state { case execModeProcessProposal: return app.processProposalState + case execModeSimulate: + // Keep the simulation context aligned with the CheckTx context while + // preserving its own store branch. + if app.checkState != nil && app.simulationState != nil { + app.simulationState.SetContext(app.checkState.Context().WithMultiStore(app.simulationState.ms)) + } + return app.simulationState default: return app.checkState }