package baseapp_test

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"testing"
	"time"

	abci "git.cw.tr/mukan-network/mukan-consensus/abci/types"
	"git.cw.tr/mukan-network/mukan-consensus/crypto/secp256k1"
	cmtprotocrypto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/crypto"
	cmtproto "git.cw.tr/mukan-network/mukan-consensus/proto/tendermint/types"
	dbm "github.com/cosmos/cosmos-db"
	protoio "github.com/cosmos/gogoproto/io"
	"github.com/cosmos/gogoproto/jsonpb"
	"github.com/cosmos/gogoproto/proto"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	errorsmod "cosmossdk.io/errors"
	"cosmossdk.io/log"
	pruningtypes "cosmossdk.io/store/pruning/types"
	"cosmossdk.io/store/snapshots"
	snapshottypes "cosmossdk.io/store/snapshots/types"
	storetypes "cosmossdk.io/store/types"

	"git.cw.tr/mukan-network/mukan-sdk/baseapp"
	baseapptestutil "git.cw.tr/mukan-network/mukan-sdk/baseapp/testutil"
	"git.cw.tr/mukan-network/mukan-sdk/baseapp/testutil/mock"
	"git.cw.tr/mukan-network/mukan-sdk/testutil"
	"git.cw.tr/mukan-network/mukan-sdk/testutil/testdata"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	sdkerrors "git.cw.tr/mukan-network/mukan-sdk/types/errors"
	"git.cw.tr/mukan-network/mukan-sdk/types/mempool"
	"git.cw.tr/mukan-network/mukan-sdk/x/auth/signing"
)

type mockABCIListener struct {
	ListenCommitFn func(context.Context, abci.ResponseCommit, []*storetypes.StoreKVPair) error
}

func (m mockABCIListener) ListenFinalizeBlock(_ context.Context, _ abci.RequestFinalizeBlock, _ abci.ResponseFinalizeBlock) error {
	return nil
}

func (m *mockABCIListener) ListenCommit(ctx context.Context, commit abci.ResponseCommit, pairs []*storetypes.StoreKVPair) error {
	return m.ListenCommitFn(ctx, commit, pairs)
}

func TestABCI_Info(t *testing.T) {
	suite := NewBaseAppSuite(t)

	reqInfo := abci.RequestInfo{}
	res, err := suite.baseApp.Info(&reqInfo)
	require.NoError(t, err)

	emptyHash := sha256.Sum256([]byte{})
	appHash := emptyHash[:]
	require.Equal(t, "", res.Version)
	require.Equal(t, t.Name(), res.GetData())
	require.Equal(t, int64(0), res.LastBlockHeight)
	require.Equal(t, appHash, res.LastBlockAppHash)
	require.Equal(t, suite.baseApp.AppVersion(), res.AppVersion)
}

func TestABCI_First_block_Height(t *testing.T) {
	suite := NewBaseAppSuite(t, baseapp.SetChainID("test-chain-id"))
	app := suite.baseApp

	_, err := app.InitChain(&abci.RequestInitChain{
		ChainId:         "test-chain-id",
		ConsensusParams: &cmtproto.ConsensusParams{Block: &cmtproto.BlockParams{MaxGas: 5000000}},
		InitialHeight:   1,
	})
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	ctx := app.GetContextForCheckTx(nil)
	require.Equal(t, int64(1), ctx.BlockHeight())
}

func TestABCI_InitChain(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	logger := log.NewTestLogger(t)
	app := baseapp.NewBaseApp(name, logger, db, nil, baseapp.SetChainID("test-chain-id"))

	capKey := storetypes.NewKVStoreKey("main")
	capKey2 := storetypes.NewKVStoreKey("key2")
	app.MountStores(capKey, capKey2)

	// set a value in the store on init chain
	key, value := []byte("hello"), []byte("goodbye")
	var initChainer sdk.InitChainer = func(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
		store := ctx.KVStore(capKey)
		store.Set(key, value)
		return &abci.ResponseInitChain{}, nil
	}

	query := abci.RequestQuery{
		Path: "/store/main/key",
		Data: key,
	}

	// initChain is nil and chain ID is wrong - errors
	_, err := app.InitChain(&abci.RequestInitChain{ChainId: "wrong-chain-id"})
	require.Error(t, err)

	// initChain is nil - nothing happens
	_, err = app.InitChain(&abci.RequestInitChain{ChainId: "test-chain-id"})
	require.NoError(t, err)
	resQ, err := app.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, 0, len(resQ.Value))

	// set initChainer and try again - should see the value
	app.SetInitChainer(initChainer)

	// stores are mounted and private members are set - sealing baseapp
	err = app.LoadLatestVersion() // needed to make stores non-nil
	require.Nil(t, err)
	require.Equal(t, int64(0), app.LastBlockHeight())

	initChainRes, err := app.InitChain(&abci.RequestInitChain{AppStateBytes: []byte("{}"), ChainId: "test-chain-id"}) // must have valid JSON genesis file, even if empty
	require.NoError(t, err)

	// The AppHash returned by a new chain is the sha256 hash of "".
	// $ echo -n '' | sha256sum
	// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
	emptyHash := sha256.Sum256([]byte{})
	appHash := emptyHash[:]
	require.NoError(t, err)

	require.Equal(t, appHash, initChainRes.AppHash)

	// assert that chainID is set correctly in InitChain
	chainID := getFinalizeBlockStateCtx(app).ChainID()
	require.Equal(t, "test-chain-id", chainID, "ChainID in deliverState not set correctly in InitChain")

	chainID = getCheckStateCtx(app).ChainID()
	require.Equal(t, "test-chain-id", chainID, "ChainID in checkState not set correctly in InitChain")

	_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{
		Hash:   initChainRes.AppHash,
		Height: 1,
	})
	require.NoError(t, err)

	_, err = app.Commit()
	require.NoError(t, err)

	resQ, err = app.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, int64(1), app.LastBlockHeight())
	require.Equal(t, value, resQ.Value)

	// reload app
	app = baseapp.NewBaseApp(name, logger, db, nil)
	app.SetInitChainer(initChainer)
	app.MountStores(capKey, capKey2)
	err = app.LoadLatestVersion() // needed to make stores non-nil
	require.Nil(t, err)
	require.Equal(t, int64(1), app.LastBlockHeight())

	// ensure we can still query after reloading
	resQ, err = app.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, value, resQ.Value)

	// commit and ensure we can still query
	_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: app.LastBlockHeight() + 1})
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	resQ, err = app.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, value, resQ.Value)
}

func TestABCI_InitChain_WithInitialHeight(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil)

	_, err := app.InitChain(
		&abci.RequestInitChain{
			InitialHeight: 3,
		},
	)
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	require.Equal(t, int64(3), app.LastBlockHeight())
}

func TestABCI_FinalizeBlock_WithInitialHeight(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil)

	_, err := app.InitChain(
		&abci.RequestInitChain{
			InitialHeight: 3,
		},
	)
	require.NoError(t, err)

	_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 4})
	require.Error(t, err, "invalid height: 4; expected: 3")

	_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 3})
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	require.Equal(t, int64(3), app.LastBlockHeight())
}

func TestABCI_FinalizeBlock_WithBeginAndEndBlocker(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil)

	app.SetBeginBlocker(func(ctx sdk.Context) (sdk.BeginBlock, error) {
		return sdk.BeginBlock{
			Events: []abci.Event{
				{
					Type: "sometype",
					Attributes: []abci.EventAttribute{
						{
							Key:   "foo",
							Value: "bar",
						},
					},
				},
			},
		}, nil
	})

	app.SetEndBlocker(func(ctx sdk.Context) (sdk.EndBlock, error) {
		return sdk.EndBlock{
			Events: []abci.Event{
				{
					Type: "anothertype",
					Attributes: []abci.EventAttribute{
						{
							Key:   "foo",
							Value: "bar",
						},
					},
				},
			},
		}, nil
	})

	_, err := app.InitChain(
		&abci.RequestInitChain{
			InitialHeight: 1,
		},
	)
	require.NoError(t, err)

	res, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.NoError(t, err)

	require.Len(t, res.Events, 2)

	require.Equal(t, "sometype", res.Events[0].Type)
	require.Equal(t, "foo", res.Events[0].Attributes[0].Key)
	require.Equal(t, "bar", res.Events[0].Attributes[0].Value)
	require.Equal(t, "mode", res.Events[0].Attributes[1].Key)
	require.Equal(t, "BeginBlock", res.Events[0].Attributes[1].Value)

	require.Equal(t, "anothertype", res.Events[1].Type)
	require.Equal(t, "foo", res.Events[1].Attributes[0].Key)
	require.Equal(t, "bar", res.Events[1].Attributes[0].Value)
	require.Equal(t, "mode", res.Events[1].Attributes[1].Key)
	require.Equal(t, "EndBlock", res.Events[1].Attributes[1].Value)

	_, err = app.Commit()
	require.NoError(t, err)

	require.Equal(t, int64(1), app.LastBlockHeight())
}

func TestABCI_ExtendVote(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil)

	app.SetExtendVoteHandler(func(ctx sdk.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
		voteExt := "foo" + hex.EncodeToString(req.Hash) + strconv.FormatInt(req.Height, 10)
		return &abci.ResponseExtendVote{VoteExtension: []byte(voteExt)}, nil
	})

	app.SetVerifyVoteExtensionHandler(func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
		// do some kind of verification here
		expectedVoteExt := "foo" + hex.EncodeToString(req.Hash) + strconv.FormatInt(req.Height, 10)
		if !bytes.Equal(req.VoteExtension, []byte(expectedVoteExt)) {
			return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, nil
		}

		return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil
	})

	app.SetParamStore(&paramStore{db: dbm.NewMemDB()})
	_, err := app.InitChain(
		&abci.RequestInitChain{
			InitialHeight: 1,
			ConsensusParams: &cmtproto.ConsensusParams{
				Abci: &cmtproto.ABCIParams{
					VoteExtensionsEnableHeight: 200,
				},
			},
		},
	)
	require.NoError(t, err)

	// Votes not enabled yet
	_, err = app.ExtendVote(context.Background(), &abci.RequestExtendVote{Height: 123, Hash: []byte("thehash")})
	require.ErrorContains(t, err, "vote extensions are not enabled")

	// First vote on the first enabled height
	res, err := app.ExtendVote(context.Background(), &abci.RequestExtendVote{Height: 200, Hash: []byte("thehash")})
	require.NoError(t, err)
	require.Len(t, res.VoteExtension, 20)

	res, err = app.ExtendVote(context.Background(), &abci.RequestExtendVote{Height: 1000, Hash: []byte("thehash")})
	require.NoError(t, err)
	require.Len(t, res.VoteExtension, 21)

	// Error during vote extension should return an empty vote extension and no error
	app.SetExtendVoteHandler(func(ctx sdk.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
		return nil, errors.New("some error")
	})
	res, err = app.ExtendVote(context.Background(), &abci.RequestExtendVote{Height: 1000, Hash: []byte("thehash")})
	require.NoError(t, err)
	require.Len(t, res.VoteExtension, 0)

	// Verify Vote Extensions
	_, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 123, VoteExtension: []byte("1234567")})
	require.ErrorContains(t, err, "vote extensions are not enabled")

	// First vote on the first enabled height
	vres, err := app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 200, Hash: []byte("thehash"), VoteExtension: []byte("foo74686568617368200")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_ACCEPT, vres.Status)

	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 1000, Hash: []byte("thehash"), VoteExtension: []byte("foo746865686173681000")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_ACCEPT, vres.Status)

	// Reject because it's just some random bytes
	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 201, Hash: []byte("thehash"), VoteExtension: []byte("12345678")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, vres.Status)

	// Reject because the verification failed (no error)
	app.SetVerifyVoteExtensionHandler(func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
		return nil, errors.New("some error")
	})
	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 201, Hash: []byte("thehash"), VoteExtension: []byte("12345678")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, vres.Status)
}

// TestABCI_OnlyVerifyVoteExtension makes sure we can call VerifyVoteExtension
// without having called ExtendVote before.
func TestABCI_OnlyVerifyVoteExtension(t *testing.T) {
	name := t.Name()
	db := dbm.NewMemDB()
	app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil)

	app.SetVerifyVoteExtensionHandler(func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
		// do some kind of verification here
		expectedVoteExt := "foo" + hex.EncodeToString(req.Hash) + strconv.FormatInt(req.Height, 10)
		if !bytes.Equal(req.VoteExtension, []byte(expectedVoteExt)) {
			return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, nil
		}

		return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil
	})

	app.SetParamStore(&paramStore{db: dbm.NewMemDB()})
	_, err := app.InitChain(
		&abci.RequestInitChain{
			InitialHeight: 1,
			ConsensusParams: &cmtproto.ConsensusParams{
				Abci: &cmtproto.ABCIParams{
					VoteExtensionsEnableHeight: 200,
				},
			},
		},
	)
	require.NoError(t, err)

	// Verify Vote Extensions
	_, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 123, VoteExtension: []byte("1234567")})
	require.ErrorContains(t, err, "vote extensions are not enabled")

	// First vote on the first enabled height
	vres, err := app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 200, Hash: []byte("thehash"), VoteExtension: []byte("foo74686568617368200")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_ACCEPT, vres.Status)

	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 1000, Hash: []byte("thehash"), VoteExtension: []byte("foo746865686173681000")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_ACCEPT, vres.Status)

	// Reject because it's just some random bytes
	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 201, Hash: []byte("thehash"), VoteExtension: []byte("12345678")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, vres.Status)

	// Reject because the verification failed (no error)
	app.SetVerifyVoteExtensionHandler(func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
		return nil, errors.New("some error")
	})
	vres, err = app.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{Height: 201, Hash: []byte("thehash"), VoteExtension: []byte("12345678")})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, vres.Status)
}

func TestABCI_GRPCQuery(t *testing.T) {
	grpcQueryOpt := func(bapp *baseapp.BaseApp) {
		testdata.RegisterQueryServer(
			bapp.GRPCQueryRouter(),
			testdata.QueryImpl{},
		)
	}

	suite := NewBaseAppSuite(t, grpcQueryOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	req := testdata.SayHelloRequest{Name: "foo"}
	reqBz, err := req.Marshal()
	require.NoError(t, err)

	resQuery, err := suite.baseApp.Query(context.TODO(), &abci.RequestQuery{
		Data: reqBz,
		Path: "/testpb.Query/SayHello",
	})
	require.NoError(t, err)
	require.Equal(t, sdkerrors.ErrInvalidHeight.ABCICode(), resQuery.Code, resQuery)
	require.Contains(t, resQuery.Log, "TestABCI_GRPCQuery is not ready; please wait for first block")

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: suite.baseApp.LastBlockHeight() + 1})
	require.NoError(t, err)
	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	reqQuery := abci.RequestQuery{
		Data: reqBz,
		Path: "/testpb.Query/SayHello",
	}

	resQuery, err = suite.baseApp.Query(context.TODO(), &reqQuery)
	require.NoError(t, err)
	require.Equal(t, abci.CodeTypeOK, resQuery.Code, resQuery)

	var res testdata.SayHelloResponse
	require.NoError(t, res.Unmarshal(resQuery.Value))
	require.Equal(t, "Hello foo!", res.Greeting)
}

func TestABCI_P2PQuery(t *testing.T) {
	addrPeerFilterOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAddrPeerFilter(func(addrport string) *abci.ResponseQuery {
			require.Equal(t, "1.1.1.1:8000", addrport)
			return &abci.ResponseQuery{Code: uint32(3)}
		})
	}

	idPeerFilterOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetIDPeerFilter(func(id string) *abci.ResponseQuery {
			require.Equal(t, "testid", id)
			return &abci.ResponseQuery{Code: uint32(4)}
		})
	}

	suite := NewBaseAppSuite(t, addrPeerFilterOpt, idPeerFilterOpt)

	addrQuery := abci.RequestQuery{
		Path: "/p2p/filter/addr/1.1.1.1:8000",
	}
	res, err := suite.baseApp.Query(context.TODO(), &addrQuery)
	require.NoError(t, err)
	require.Equal(t, uint32(3), res.Code)

	idQuery := abci.RequestQuery{
		Path: "/p2p/filter/id/testid",
	}
	res, err = suite.baseApp.Query(context.TODO(), &idQuery)
	require.NoError(t, err)
	require.Equal(t, uint32(4), res.Code)
}

func TestBaseApp_PrepareCheckState(t *testing.T) {
	db := dbm.NewMemDB()
	name := t.Name()
	logger := log.NewTestLogger(t)

	cp := &cmtproto.ConsensusParams{
		Block: &cmtproto.BlockParams{
			MaxGas: 5000000,
		},
	}

	app := baseapp.NewBaseApp(name, logger, db, nil)
	app.SetParamStore(&paramStore{db: dbm.NewMemDB()})
	_, err := app.InitChain(&abci.RequestInitChain{
		ConsensusParams: cp,
	})
	require.NoError(t, err)

	wasPrepareCheckStateCalled := false
	app.SetPrepareCheckStater(func(ctx sdk.Context) {
		wasPrepareCheckStateCalled = true
	})
	app.Seal()

	_, err = app.Commit()
	require.NoError(t, err)
	require.Equal(t, true, wasPrepareCheckStateCalled)
}

func TestBaseApp_Precommit(t *testing.T) {
	db := dbm.NewMemDB()
	name := t.Name()
	logger := log.NewTestLogger(t)

	cp := &cmtproto.ConsensusParams{
		Block: &cmtproto.BlockParams{
			MaxGas: 5000000,
		},
	}

	app := baseapp.NewBaseApp(name, logger, db, nil)
	app.SetParamStore(&paramStore{db: dbm.NewMemDB()})
	_, err := app.InitChain(&abci.RequestInitChain{
		ConsensusParams: cp,
	})
	require.NoError(t, err)

	wasPrecommiterCalled := false
	app.SetPrecommiter(func(ctx sdk.Context) {
		wasPrecommiterCalled = true
	})
	app.Seal()

	_, err = app.Commit()
	require.NoError(t, err)
	require.Equal(t, true, wasPrecommiterCalled)
}

func TestABCI_CheckTx(t *testing.T) {
	// This ante handler reads the key and checks that the value matches the
	// current counter. This ensures changes to the KVStore persist across
	// successive CheckTx runs.
	counterKey := []byte("counter-key")
	anteOpt := func(bapp *baseapp.BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, counterKey)) }
	suite := NewBaseAppSuite(t, anteOpt)

	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImpl{t, capKey1, counterKey})

	nTxs := int64(5)
	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	for i := int64(0); i < nTxs; i++ {
		tx := newTxCounter(t, suite.txConfig, i, 0) // no messages
		txBytes, err := suite.txConfig.TxEncoder()(tx)
		require.NoError(t, err)

		r, err := suite.baseApp.CheckTx(&abci.RequestCheckTx{Tx: txBytes})
		require.NoError(t, err)
		require.True(t, r.IsOK(), fmt.Sprintf("%v", r))
		require.Empty(t, r.GetEvents())
	}

	checkStateStore := getCheckStateCtx(suite.baseApp).KVStore(capKey1)
	storedCounter := getIntFromStore(t, checkStateStore, counterKey)

	// ensure AnteHandler ran
	require.Equal(t, nTxs, storedCounter)

	// if a block is committed, CheckTx state should be reset
	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
		Hash:   []byte("hash"),
	})
	require.NoError(t, err)

	require.NotNil(t, getCheckStateCtx(suite.baseApp).BlockGasMeter(), "block gas meter should have been set to checkState")
	require.NotEmpty(t, getCheckStateCtx(suite.baseApp).HeaderHash())

	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	checkStateStore = getCheckStateCtx(suite.baseApp).KVStore(capKey1)
	storedBytes := checkStateStore.Get(counterKey)
	require.Nil(t, storedBytes)
}

func TestABCI_FinalizeBlock_DeliverTx(t *testing.T) {
	anteKey := []byte("ante-key")
	anteOpt := func(bapp *baseapp.BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey)) }
	suite := NewBaseAppSuite(t, anteOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	deliverKey := []byte("deliver-key")
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImpl{t, capKey1, deliverKey})

	nBlocks := 3
	txPerHeight := 5

	for blockN := range nBlocks {

		txs := [][]byte{}
		for i := range txPerHeight {
			counter := int64(blockN*txPerHeight + i)
			tx := newTxCounter(t, suite.txConfig, counter, counter)

			txBytes, err := suite.txConfig.TxEncoder()(tx)
			require.NoError(t, err)

			txs = append(txs, txBytes)
		}

		res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
			Height: int64(blockN) + 1,
			Txs:    txs,
		})
		require.NoError(t, err)

		for i := range txPerHeight {
			counter := int64(blockN*txPerHeight + i)
			require.True(t, res.TxResults[i].IsOK(), fmt.Sprintf("%v", res))

			events := res.TxResults[i].GetEvents()
			require.Len(t, events, 3, "should contain ante handler, message type and counter events respectively")
			require.Equal(t, sdk.MarkEventsToIndex(counterEvent("ante_handler", counter).ToABCIEvents(), map[string]struct{}{})[0], events[0], "ante handler event")
			require.Equal(t, sdk.MarkEventsToIndex(counterEvent(sdk.EventTypeMessage, counter).ToABCIEvents(), map[string]struct{}{})[0].Attributes[0], events[2].Attributes[0], "msg handler update counter event")
		}

		_, err = suite.baseApp.Commit()
		require.NoError(t, err)
	}
}

func TestABCI_FinalizeBlock_MultiMsg(t *testing.T) {
	anteKey := []byte("ante-key")
	anteOpt := func(bapp *baseapp.BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey)) }
	suite := NewBaseAppSuite(t, anteOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	deliverKey := []byte("deliver-key")
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImpl{t, capKey1, deliverKey})

	deliverKey2 := []byte("deliver-key2")
	baseapptestutil.RegisterCounter2Server(suite.baseApp.MsgServiceRouter(), Counter2ServerImpl{t, capKey1, deliverKey2})

	// run a multi-msg tx
	// with all msgs the same route
	tx := newTxCounter(t, suite.txConfig, 0, 0, 1, 2)
	txBytes, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
		Txs:    [][]byte{txBytes},
	})
	require.NoError(t, err)

	store := getFinalizeBlockStateCtx(suite.baseApp).KVStore(capKey1)

	// tx counter only incremented once
	txCounter := getIntFromStore(t, store, anteKey)
	require.Equal(t, int64(1), txCounter)

	// msg counter incremented three times
	msgCounter := getIntFromStore(t, store, deliverKey)
	require.Equal(t, int64(3), msgCounter)

	// replace the second message with a Counter2
	tx = newTxCounter(t, suite.txConfig, 1, 3)

	builder := suite.txConfig.NewTxBuilder()
	msgs := tx.GetMsgs()
	_, _, addr := testdata.KeyTestPubAddr()
	msgs = append(msgs, &baseapptestutil.MsgCounter2{Counter: 0, Signer: addr.String()})
	msgs = append(msgs, &baseapptestutil.MsgCounter2{Counter: 1, Signer: addr.String()})

	require.NoError(t, builder.SetMsgs(msgs...))
	builder.SetMemo(tx.GetMemo())
	setTxSignature(t, builder, 0)

	txBytes, err = suite.txConfig.TxEncoder()(builder.GetTx())
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
		Txs:    [][]byte{txBytes},
	})
	require.NoError(t, err)

	store = getFinalizeBlockStateCtx(suite.baseApp).KVStore(capKey1)

	// tx counter only incremented once
	txCounter = getIntFromStore(t, store, anteKey)
	require.Equal(t, int64(2), txCounter)

	// original counter increments by one
	// new counter increments by two
	msgCounter = getIntFromStore(t, store, deliverKey)
	require.Equal(t, int64(4), msgCounter)

	msgCounter2 := getIntFromStore(t, store, deliverKey2)
	require.Equal(t, int64(2), msgCounter2)
}

func TestABCI_Query_SimulateTx(t *testing.T) {
	gasConsumed := uint64(5)
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(gasConsumed))
			return newCtx, err
		})
	}
	suite := NewBaseAppSuite(t, anteOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{gasConsumed})

	nBlocks := 3
	for blockN := range nBlocks {
		count := int64(blockN + 1)

		tx := newTxCounter(t, suite.txConfig, count, count)

		txBytes, err := suite.txConfig.TxEncoder()(tx)
		require.Nil(t, err)

		// simulate a message, check gas reported
		gInfo, result, err := suite.baseApp.Simulate(txBytes)
		require.NoError(t, err)
		require.NotNil(t, result)
		require.Equal(t, gasConsumed, gInfo.GasUsed)

		// simulate again, same result
		gInfo, result, err = suite.baseApp.Simulate(txBytes)
		require.NoError(t, err)
		require.NotNil(t, result)
		require.Equal(t, gasConsumed, gInfo.GasUsed)

		// simulate by calling Query with encoded tx
		query := abci.RequestQuery{
			Path: "/app/simulate",
			Data: txBytes,
		}
		queryResult, err := suite.baseApp.Query(context.TODO(), &query)
		require.NoError(t, err)
		require.True(t, queryResult.IsOK(), queryResult.Log)

		var simRes sdk.SimulationResponse
		require.NoError(t, jsonpb.Unmarshal(strings.NewReader(string(queryResult.Value)), &simRes))

		require.Equal(t, gInfo, simRes.GasInfo)
		require.Equal(t, result.Log, simRes.Result.Log)
		require.Equal(t, result.Events, simRes.Result.Events)
		require.True(t, bytes.Equal(result.Data, simRes.Result.Data))

		_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: count})
		require.NoError(t, err)
		_, err = suite.baseApp.Commit()
		require.NoError(t, err)
	}
}

func TestABCI_InvalidTransaction(t *testing.T) {
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			return newCtx, err
		})
	}

	suite := NewBaseAppSuite(t, anteOpt)
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
	})
	require.NoError(t, err)

	// malformed transaction bytes
	{
		bz := []byte("example vote extension")
		result, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
			Height: 1,
			Txs:    [][]byte{bz},
		})

		require.EqualValues(t, sdkerrors.ErrTxDecode.Codespace(), result.TxResults[0].Codespace, err)
		require.EqualValues(t, sdkerrors.ErrTxDecode.ABCICode(), result.TxResults[0].Code, err)
		require.EqualValues(t, 0, result.TxResults[0].GasUsed, err)
		require.EqualValues(t, 0, result.TxResults[0].GasWanted, err)
	}
	// transaction with no messages
	{
		emptyTx := suite.txConfig.NewTxBuilder().GetTx()
		bz, err := suite.txConfig.TxEncoder()(emptyTx)
		require.NoError(t, err)
		result, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
			Height: 1,
			Txs:    [][]byte{bz},
		})
		require.EqualValues(t, sdkerrors.ErrInvalidRequest.Codespace(), result.TxResults[0].Codespace, err)
		require.EqualValues(t, sdkerrors.ErrInvalidRequest.ABCICode(), result.TxResults[0].Code, err)
	}

	// transaction where ValidateBasic fails
	{
		testCases := []struct {
			tx   signing.Tx
			fail bool
		}{
			{newTxCounter(t, suite.txConfig, 0, 0), false},
			{newTxCounter(t, suite.txConfig, -1, 0), false},
			{newTxCounter(t, suite.txConfig, 100, 100), false},
			{newTxCounter(t, suite.txConfig, 100, 5, 4, 3, 2, 1), false},

			{newTxCounter(t, suite.txConfig, 0, -1), true},
			{newTxCounter(t, suite.txConfig, 0, 1, -2), true},
			{newTxCounter(t, suite.txConfig, 0, 1, 2, -10, 5), true},
		}

		for _, testCase := range testCases {
			tx := testCase.tx
			_, result, err := suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), tx)

			if testCase.fail {
				require.Error(t, err)

				space, code, _ := errorsmod.ABCIInfo(err, false)
				require.EqualValues(t, sdkerrors.ErrInvalidSequence.Codespace(), space, err)
				require.EqualValues(t, sdkerrors.ErrInvalidSequence.ABCICode(), code, err)
			} else {
				require.NotNil(t, result)
			}
		}
	}

	// transaction with no known route
	{
		txBuilder := suite.txConfig.NewTxBuilder()
		_, _, addr := testdata.KeyTestPubAddr()
		require.NoError(t, txBuilder.SetMsgs(&baseapptestutil.MsgCounter2{Signer: addr.String()}))
		setTxSignature(t, txBuilder, 0)
		unknownRouteTx := txBuilder.GetTx()

		_, result, err := suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), unknownRouteTx)
		require.Error(t, err)
		require.Nil(t, result)

		space, code, _ := errorsmod.ABCIInfo(err, false)
		require.EqualValues(t, sdkerrors.ErrUnknownRequest.Codespace(), space, err)
		require.EqualValues(t, sdkerrors.ErrUnknownRequest.ABCICode(), code, err)

		txBuilder = suite.txConfig.NewTxBuilder()
		require.NoError(t, txBuilder.SetMsgs(
			&baseapptestutil.MsgCounter{Signer: addr.String()},
			&baseapptestutil.MsgCounter2{Signer: addr.String()},
		))
		setTxSignature(t, txBuilder, 0)
		unknownRouteTx = txBuilder.GetTx()

		_, result, err = suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), unknownRouteTx)
		require.Error(t, err)
		require.Nil(t, result)

		space, code, _ = errorsmod.ABCIInfo(err, false)
		require.EqualValues(t, sdkerrors.ErrUnknownRequest.Codespace(), space, err)
		require.EqualValues(t, sdkerrors.ErrUnknownRequest.ABCICode(), code, err)
	}

	// Transaction with an unregistered message
	{
		txBuilder := suite.txConfig.NewTxBuilder()
		require.NoError(t, txBuilder.SetMsgs(&testdata.MsgCreateDog{}))
		tx := txBuilder.GetTx()

		_, _, err := suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), tx)
		require.Error(t, err)
		space, code, _ := errorsmod.ABCIInfo(err, false)
		require.EqualValues(t, sdkerrors.ErrUnknownRequest.ABCICode(), code)
		require.EqualValues(t, sdkerrors.ErrTxDecode.Codespace(), space)
	}
}

func TestABCI_TxGasLimits(t *testing.T) {
	gasGranted := uint64(10)
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(gasGranted))

			// AnteHandlers must have their own defer/recover in order for the BaseApp
			// to know how much gas was used! This is because the GasMeter is created in
			// the AnteHandler, but if it panics the context won't be set properly in
			// runTx's recover call.
			defer func() {
				if r := recover(); r != nil {
					switch rType := r.(type) {
					case storetypes.ErrorOutOfGas:
						err = errorsmod.Wrapf(sdkerrors.ErrOutOfGas, "out of gas in location: %v", rType.Descriptor)
					default:
						panic(r)
					}
				}
			}()

			count, _ := parseTxMemo(t, tx)
			newCtx.GasMeter().ConsumeGas(uint64(count), "counter-ante")

			return newCtx, nil
		})
	}

	suite := NewBaseAppSuite(t, anteOpt)
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
	})
	require.NoError(t, err)

	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	testCases := []struct {
		tx      signing.Tx
		gasUsed int64
		fail    bool
	}{
		{newTxCounter(t, suite.txConfig, 0, 0), 0, false},
		{newTxCounter(t, suite.txConfig, 1, 1), 2, false},
		{newTxCounter(t, suite.txConfig, 9, 1), 10, false},
		{newTxCounter(t, suite.txConfig, 1, 9), 10, false},
		{newTxCounter(t, suite.txConfig, 10, 0), 10, false},

		{newTxCounter(t, suite.txConfig, 9, 2), 11, true},
		{newTxCounter(t, suite.txConfig, 2, 9), 11, true},
		// {newTxCounter(t, suite.txConfig, 9, 1, 1), 11, true},
		// {newTxCounter(t, suite.txConfig, 1, 8, 1, 1), 11, true},
		//  {newTxCounter(t, suite.txConfig, 11, 0), 11, true},
		//  {newTxCounter(t, suite.txConfig, 0, 11), 11, true},
		//  {newTxCounter(t, suite.txConfig, 0, 5, 11), 16, true},
	}

	txs := [][]byte{}
	for _, tc := range testCases {
		tx := tc.tx
		bz, err := suite.txConfig.TxEncoder()(tx)
		require.NoError(t, err)
		txs = append(txs, bz)
	}

	// Deliver the txs
	res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 2,
		Txs:    txs,
	})

	require.NoError(t, err)

	for i, tc := range testCases {

		result := res.TxResults[i]

		require.Equal(t, tc.gasUsed, result.GasUsed, fmt.Sprintf("tc #%d; gas: %v, result: %v, err: %s", i, result.GasUsed, result, err))

		// check for out of gas
		if !tc.fail {
			require.NotNil(t, result, fmt.Sprintf("%d: %v, %v", i, tc, err))
		} else {
			require.EqualValues(t, sdkerrors.ErrOutOfGas.Codespace(), result.Codespace, err)
			require.EqualValues(t, sdkerrors.ErrOutOfGas.ABCICode(), result.Code, err)
		}
	}
}

func TestABCI_MaxBlockGasLimits(t *testing.T) {
	gasGranted := uint64(10)
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(gasGranted))

			defer func() {
				if r := recover(); r != nil {
					switch rType := r.(type) {
					case storetypes.ErrorOutOfGas:
						err = errorsmod.Wrapf(sdkerrors.ErrOutOfGas, "out of gas in location: %v", rType.Descriptor)
					default:
						panic(r)
					}
				}
			}()

			count, _ := parseTxMemo(t, tx)
			newCtx.GasMeter().ConsumeGas(uint64(count), "counter-ante")

			return newCtx, err
		})
	}

	suite := NewBaseAppSuite(t, anteOpt)
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{
			Block: &cmtproto.BlockParams{
				MaxGas: 100,
			},
		},
	})
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.NoError(t, err)

	testCases := []struct {
		tx                signing.Tx
		numDelivers       int
		gasUsedPerDeliver uint64
		fail              bool
		failAfterDeliver  int
	}{
		{newTxCounter(t, suite.txConfig, 0, 0), 0, 0, false, 0},
		{newTxCounter(t, suite.txConfig, 9, 1), 2, 10, false, 0},
		{newTxCounter(t, suite.txConfig, 10, 0), 3, 10, false, 0},
		{newTxCounter(t, suite.txConfig, 10, 0), 10, 10, false, 0},
		{newTxCounter(t, suite.txConfig, 2, 7), 11, 9, false, 0},
		// {newTxCounter(t, suite.txConfig, 10, 0), 10, 10, false, 0}, // hit the limit but pass

		// {newTxCounter(t, suite.txConfig, 10, 0), 11, 10, true, 10},
		// {newTxCounter(t, suite.txConfig, 10, 0), 15, 10, true, 10},
		// {newTxCounter(t, suite.txConfig, 9, 0), 12, 9, true, 11}, // fly past the limit
	}

	for i, tc := range testCases {
		tx := tc.tx

		// reset block gas
		_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: suite.baseApp.LastBlockHeight() + 1})
		require.NoError(t, err)

		// execute the transaction multiple times
		for j := range tc.numDelivers {

			_, result, err := suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), tx)

			ctx := getFinalizeBlockStateCtx(suite.baseApp)

			// check for failed transactions
			if tc.fail && (j+1) > tc.failAfterDeliver {
				require.Error(t, err, fmt.Sprintf("tc #%d; result: %v, err: %s", i, result, err))
				require.Nil(t, tx, fmt.Sprintf("tc #%d; result: %v, err: %s", i, result, err))

				space, code, _ := errorsmod.ABCIInfo(err, false)
				require.EqualValues(t, sdkerrors.ErrOutOfGas.Codespace(), space, err)
				require.EqualValues(t, sdkerrors.ErrOutOfGas.ABCICode(), code, err)
				require.True(t, ctx.BlockGasMeter().IsOutOfGas())
			} else {
				// check gas used and wanted
				blockGasUsed := ctx.BlockGasMeter().GasConsumed()
				expBlockGasUsed := tc.gasUsedPerDeliver * uint64(j+1)
				require.Equal(
					t, expBlockGasUsed, blockGasUsed,
					fmt.Sprintf("%d,%d: %v, %v, %v, %v", i, j, tc, expBlockGasUsed, blockGasUsed, result),
				)

				require.NotNil(t, tx, fmt.Sprintf("tc #%d; currDeliver: %d, result: %v, err: %s", i, j, result, err))
				require.False(t, ctx.BlockGasMeter().IsPastLimit())
			}
		}
	}
}

func TestABCI_GasConsumptionBadTx(t *testing.T) {
	gasWanted := uint64(5)
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(gasWanted))

			defer func() {
				if r := recover(); r != nil {
					switch rType := r.(type) {
					case storetypes.ErrorOutOfGas:
						log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor)
						err = errorsmod.Wrap(sdkerrors.ErrOutOfGas, log)
					default:
						panic(r)
					}
				}
			}()

			counter, failOnAnte := parseTxMemo(t, tx)
			newCtx.GasMeter().ConsumeGas(uint64(counter), "counter-ante")
			if failOnAnte {
				return newCtx, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "ante handler failure")
			}

			return newCtx, err
		})
	}

	suite := NewBaseAppSuite(t, anteOpt)
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{
			Block: &cmtproto.BlockParams{
				MaxGas: 9,
			},
		},
	})
	require.NoError(t, err)

	tx := newTxCounter(t, suite.txConfig, 5, 0)
	tx = setFailOnAnte(t, suite.txConfig, tx, true)
	txBytes, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	// require next tx to fail due to black gas limit
	tx = newTxCounter(t, suite.txConfig, 5, 0)
	txBytes2, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: suite.baseApp.LastBlockHeight() + 1,
		Txs:    [][]byte{txBytes, txBytes2},
	})
	require.NoError(t, err)
}

func TestABCI_Query(t *testing.T) {
	key, value := []byte("hello"), []byte("goodbye")
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
			store := ctx.KVStore(capKey1)
			store.Set(key, value)
			return newCtx, err
		})
	}

	suite := NewBaseAppSuite(t, anteOpt)
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImplGasMeterOnly{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	// NOTE: "/store/key1" tells us KVStore
	// and the final "/key" says to use the data as the
	// key in the given KVStore ...
	query := abci.RequestQuery{
		Path: "/store/key1/key",
		Data: key,
	}
	tx := newTxCounter(t, suite.txConfig, 0, 0)

	// query is empty before we do anything
	res, err := suite.baseApp.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, 0, len(res.Value))

	// query is still empty after a CheckTx
	_, resTx, err := suite.baseApp.SimCheck(suite.txConfig.TxEncoder(), tx)
	require.NoError(t, err)
	require.NotNil(t, resTx)

	res, err = suite.baseApp.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, 0, len(res.Value))

	bz, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 1,
		Txs:    [][]byte{bz},
	})
	require.NoError(t, err)

	res, err = suite.baseApp.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, 0, len(res.Value))

	// query returns correct value after Commit
	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	res, err = suite.baseApp.Query(context.TODO(), &query)
	require.NoError(t, err)
	require.Equal(t, value, res.Value)
}

func TestABCI_GetBlockRetentionHeight(t *testing.T) {
	logger := log.NewTestLogger(t)
	db := dbm.NewMemDB()
	name := t.Name()

	snapshotStore, err := snapshots.NewStore(dbm.NewMemDB(), testutil.GetTempDir(t))
	require.NoError(t, err)

	testCases := map[string]struct {
		bapp         *baseapp.BaseApp
		maxAgeBlocks int64
		commitHeight int64
		expected     int64
	}{
		"defaults": {
			bapp:         baseapp.NewBaseApp(name, logger, db, nil),
			maxAgeBlocks: 0,
			commitHeight: 499000,
			expected:     0,
		},
		"pruning unbonding time only": {
			bapp:         baseapp.NewBaseApp(name, logger, db, nil, baseapp.SetMinRetainBlocks(1)),
			maxAgeBlocks: 362880,
			commitHeight: 499000,
			expected:     136120,
		},
		"pruning iavl snapshot only": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetPruning(pruningtypes.NewPruningOptions(pruningtypes.PruningNothing)),
				baseapp.SetMinRetainBlocks(1),
				baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(10000, 1)),
			),
			maxAgeBlocks: 0,
			commitHeight: 499000,
			expected:     489000,
		},
		"pruning state sync snapshot only": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
				baseapp.SetMinRetainBlocks(1),
			),
			maxAgeBlocks: 0,
			commitHeight: 499000,
			expected:     349000,
		},
		"pruning min retention only": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetMinRetainBlocks(400000),
			),
			maxAgeBlocks: 0,
			commitHeight: 499000,
			expected:     99000,
		},
		"pruning all conditions": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
				baseapp.SetMinRetainBlocks(400000),
				baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
			),
			maxAgeBlocks: 362880,
			commitHeight: 499000,
			expected:     99000,
		},
		"no pruning due to no persisted state": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
				baseapp.SetMinRetainBlocks(400000),
				baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
			),
			maxAgeBlocks: 362880,
			commitHeight: 10000,
			expected:     0,
		},
		"no pruning due to min retain blocks equal to commit height": {
			bapp:         baseapp.NewBaseApp(name, logger, db, nil, baseapp.SetMinRetainBlocks(499000)),
			maxAgeBlocks: 362880,
			commitHeight: 499000,
			expected:     0,
		},
		"no pruning due to min retain blocks greater than commit height": {
			bapp:         baseapp.NewBaseApp(name, logger, db, nil, baseapp.SetMinRetainBlocks(499001)),
			maxAgeBlocks: 362880,
			commitHeight: 499000,
			expected:     0,
		},
		"disable pruning": {
			bapp: baseapp.NewBaseApp(
				name, logger, db, nil,
				baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
				baseapp.SetMinRetainBlocks(0),
				baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
			),
			maxAgeBlocks: 362880,
			commitHeight: 499000,
			expected:     0,
		},
	}

	for name, tc := range testCases {

		tc.bapp.SetParamStore(&paramStore{db: dbm.NewMemDB()})
		_, err = tc.bapp.InitChain(&abci.RequestInitChain{
			ConsensusParams: &cmtproto.ConsensusParams{
				Evidence: &cmtproto.EvidenceParams{
					MaxAgeNumBlocks: tc.maxAgeBlocks,
				},
			},
		})
		require.NoError(t, err)

		t.Run(name, func(t *testing.T) {
			require.Equal(t, tc.expected, tc.bapp.GetBlockRetentionHeight(tc.commitHeight))
		})
	}
}

// Verifies that PrepareCheckState is called with the checkState.
func TestPrepareCheckStateCalledWithCheckState(t *testing.T) {
	t.Parallel()

	logger := log.NewTestLogger(t)
	db := dbm.NewMemDB()
	name := t.Name()
	app := baseapp.NewBaseApp(name, logger, db, nil)

	wasPrepareCheckStateCalled := false
	app.SetPrepareCheckStater(func(ctx sdk.Context) {
		require.Equal(t, true, ctx.IsCheckTx())
		wasPrepareCheckStateCalled = true
	})

	_, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	require.Equal(t, true, wasPrepareCheckStateCalled)
}

// Verifies that the Precommiter is called with the deliverState.
func TestPrecommiterCalledWithDeliverState(t *testing.T) {
	t.Parallel()

	logger := log.NewTestLogger(t)
	db := dbm.NewMemDB()
	name := t.Name()
	app := baseapp.NewBaseApp(name, logger, db, nil)

	wasPrecommiterCalled := false
	app.SetPrecommiter(func(ctx sdk.Context) {
		require.Equal(t, false, ctx.IsCheckTx())
		require.Equal(t, false, ctx.IsReCheckTx())
		wasPrecommiterCalled = true
	})

	_, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.NoError(t, err)
	_, err = app.Commit()
	require.NoError(t, err)

	require.Equal(t, true, wasPrecommiterCalled)
}

func TestABCI_Proposal_HappyPath(t *testing.T) {
	anteKey := []byte("ante-key")
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
	}

	suite := NewBaseAppSuite(t, anteOpt, baseapp.SetMempool(pool))
	baseapptestutil.RegisterKeyValueServer(suite.baseApp.MsgServiceRouter(), MsgKeyValueImpl{})
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	tx := newTxCounter(t, suite.txConfig, 0, 1)
	txBytes, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	reqCheckTx := abci.RequestCheckTx{
		Tx:   txBytes,
		Type: abci.CheckTxType_New,
	}
	_, err = suite.baseApp.CheckTx(&reqCheckTx)
	require.NoError(t, err)

	tx2 := newTxCounter(t, suite.txConfig, 1, 1)

	tx2Bytes, err := suite.txConfig.TxEncoder()(tx2)
	require.NoError(t, err)

	err = pool.Insert(sdk.Context{}, tx2)
	require.NoError(t, err)

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1,
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 2, len(resPrepareProposal.Txs))

	reqProposalTxBytes := [2][]byte{
		txBytes,
		tx2Bytes,
	}
	reqProcessProposal := abci.RequestProcessProposal{
		Txs:    reqProposalTxBytes[:],
		Height: reqPrepareProposal.Height,
	}

	resProcessProposal, err := suite.baseApp.ProcessProposal(&reqProcessProposal)
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)

	// the same txs as in PrepareProposal
	res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: suite.baseApp.LastBlockHeight() + 1,
		Txs:    reqProposalTxBytes[:],
	})
	require.NoError(t, err)

	require.Equal(t, 0, pool.CountTx())

	require.NotEmpty(t, res.TxResults[0].Events)
	require.True(t, res.TxResults[0].IsOK(), fmt.Sprintf("%v", res))
}

func TestABCI_Proposal_Read_State_PrepareProposal(t *testing.T) {
	someKey := []byte("some-key")

	setInitChainerOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetInitChainer(func(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
			ctx.KVStore(capKey1).Set(someKey, []byte("foo"))
			return &abci.ResponseInitChain{}, nil
		})
	}

	prepareOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			value := ctx.KVStore(capKey1).Get(someKey)
			// We should be able to access any state written in InitChain
			require.Equal(t, "foo", string(value))
			return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil
		})
	}

	suite := NewBaseAppSuite(t, setInitChainerOpt, prepareOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		InitialHeight:   1,
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1, // this value can't be 0
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 0, len(resPrepareProposal.Txs))

	reqProposalTxBytes := [][]byte{}
	reqProcessProposal := abci.RequestProcessProposal{
		Txs:    reqProposalTxBytes,
		Height: reqPrepareProposal.Height,
	}

	resProcessProposal, err := suite.baseApp.ProcessProposal(&reqProcessProposal)
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)
}

func TestABCI_Proposals_WithVE(t *testing.T) {
	someVoteExtension := []byte("some-vote-extension")

	setInitChainerOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetInitChainer(func(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
			return &abci.ResponseInitChain{}, nil
		})
	}

	prepareOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			// Inject the vote extension to the beginning of the proposal
			txs := make([][]byte, len(req.Txs)+1)
			txs[0] = someVoteExtension
			copy(txs[1:], req.Txs)

			return &abci.ResponsePrepareProposal{Txs: txs}, nil
		})

		bapp.SetProcessProposal(func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
			// Check that the vote extension is still there
			require.Equal(t, someVoteExtension, req.Txs[0])
			return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
		})
	}

	suite := NewBaseAppSuite(t, setInitChainerOpt, prepareOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		InitialHeight:   1,
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 100000,
		Height:     1, // this value can't be 0
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 1, len(resPrepareProposal.Txs))

	reqProcessProposal := abci.RequestProcessProposal{
		Txs:    resPrepareProposal.Txs,
		Height: reqPrepareProposal.Height,
	}
	resProcessProposal, err := suite.baseApp.ProcessProposal(&reqProcessProposal)
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)

	// Run finalize block and ensure that the vote extension is still there and that
	// the proposal is accepted
	result, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Txs:    resPrepareProposal.Txs,
		Height: reqPrepareProposal.Height,
	})
	require.NoError(t, err)
	require.Equal(t, 1, len(result.TxResults))
	require.EqualValues(t, sdkerrors.ErrTxDecode.Codespace(), result.TxResults[0].Codespace, err)
	require.EqualValues(t, sdkerrors.ErrTxDecode.ABCICode(), result.TxResults[0].Code, err)
	require.EqualValues(t, 0, result.TxResults[0].GasUsed, err)
	require.EqualValues(t, 0, result.TxResults[0].GasWanted, err)
}

func TestABCI_PrepareProposal_ReachedMaxBytes(t *testing.T) {
	anteKey := []byte("ante-key")
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
	}

	suite := NewBaseAppSuite(t, anteOpt, baseapp.SetMempool(pool))
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	for i := range 100 {
		tx2 := newTxCounter(t, suite.txConfig, int64(i), int64(i))
		err := pool.Insert(sdk.Context{}, tx2)
		require.NoError(t, err)
	}

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1500,
		Height:     1,
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 8, len(resPrepareProposal.Txs))
}

func TestABCI_PrepareProposal_BadEncoding(t *testing.T) {
	anteKey := []byte("ante-key")
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
	}

	suite := NewBaseAppSuite(t, anteOpt, baseapp.SetMempool(pool))
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	tx := newTxCounter(t, suite.txConfig, 0, 0)
	err = pool.Insert(sdk.Context{}, tx)
	require.NoError(t, err)

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1,
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 1, len(resPrepareProposal.Txs))
}

func TestABCI_PrepareProposal_OverGasUnderBytes(t *testing.T) {
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	suite := NewBaseAppSuite(t, baseapp.SetMempool(pool))
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	// set max block gas limit to 99, this will allow 9 txs of 10 gas each.
	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{
			Block: &cmtproto.BlockParams{MaxGas: 99},
		},
	})

	require.NoError(t, err)
	// insert 100 txs, each with a gas limit of 10
	_, _, addr := testdata.KeyTestPubAddr()
	for i := int64(0); i < 100; i++ {
		msg := &baseapptestutil.MsgCounter{Counter: i, FailOnHandler: false, Signer: addr.String()}
		msgs := []sdk.Msg{msg}

		builder := suite.txConfig.NewTxBuilder()
		err = builder.SetMsgs(msgs...)
		require.NoError(t, err)
		builder.SetMemo("counter=" + strconv.FormatInt(i, 10) + "&failOnAnte=false")
		builder.SetGasLimit(10)
		setTxSignature(t, builder, uint64(i))

		err := pool.Insert(sdk.Context{}, builder.GetTx())
		require.NoError(t, err)
	}

	// ensure we only select transactions that fit within the block gas limit
	res, err := suite.baseApp.PrepareProposal(&abci.RequestPrepareProposal{
		MaxTxBytes: 1_000_000, // large enough to ignore restriction
		Height:     1,
	})
	require.NoError(t, err)

	// Should include 9 transactions
	require.Len(t, res.Txs, 9, "invalid number of transactions returned")
}

func TestABCI_PrepareProposal_MaxGas(t *testing.T) {
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	suite := NewBaseAppSuite(t, baseapp.SetMempool(pool))
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	// set max block gas limit to 100
	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{
			Block: &cmtproto.BlockParams{MaxGas: 100},
		},
	})
	require.NoError(t, err)

	// insert 100 txs, each with a gas limit of 10
	_, _, addr := testdata.KeyTestPubAddr()
	for i := int64(0); i < 100; i++ {
		msg := &baseapptestutil.MsgCounter{Counter: i, FailOnHandler: false, Signer: addr.String()}
		msgs := []sdk.Msg{msg}

		builder := suite.txConfig.NewTxBuilder()
		require.NoError(t, builder.SetMsgs(msgs...))
		builder.SetMemo("counter=" + strconv.FormatInt(i, 10) + "&failOnAnte=false")
		builder.SetGasLimit(10)
		setTxSignature(t, builder, uint64(i))

		err := pool.Insert(sdk.Context{}, builder.GetTx())
		require.NoError(t, err)
	}

	// ensure we only select transactions that fit within the block gas limit
	res, err := suite.baseApp.PrepareProposal(&abci.RequestPrepareProposal{
		MaxTxBytes: 1_000_000, // large enough to ignore restriction
		Height:     1,
	})
	require.NoError(t, err)
	require.Len(t, res.Txs, 10, "invalid number of transactions returned")
}

func TestABCI_PrepareProposal_Failures(t *testing.T) {
	anteKey := []byte("ante-key")
	pool := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
	}

	suite := NewBaseAppSuite(t, anteOpt, baseapp.SetMempool(pool))
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	tx := newTxCounter(t, suite.txConfig, 0, 0)
	txBytes, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	reqCheckTx := abci.RequestCheckTx{
		Tx:   txBytes,
		Type: abci.CheckTxType_New,
	}
	checkTxRes, err := suite.baseApp.CheckTx(&reqCheckTx)
	require.NoError(t, err)
	require.True(t, checkTxRes.IsOK())

	failTx := newTxCounter(t, suite.txConfig, 1, 1)
	failTx = setFailOnAnte(t, suite.txConfig, failTx, true)

	err = pool.Insert(sdk.Context{}, failTx)
	require.NoError(t, err)
	require.Equal(t, 2, pool.CountTx())

	req := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1,
	}
	res, err := suite.baseApp.PrepareProposal(&req)
	require.NoError(t, err)
	require.Equal(t, 1, len(res.Txs))
}

func TestABCI_PrepareProposal_PanicRecovery(t *testing.T) {
	prepareOpt := func(app *baseapp.BaseApp) {
		app.SetPrepareProposal(func(ctx sdk.Context, rpp *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			panic(errors.New("test"))
		})
	}
	suite := NewBaseAppSuite(t, prepareOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	req := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1,
	}

	require.NotPanics(t, func() {
		res, err := suite.baseApp.PrepareProposal(&req)
		require.NoError(t, err)
		require.Equal(t, req.Txs, res.Txs)
	})
}

func TestABCI_PrepareProposal_VoteExtensions(t *testing.T) {
	// set up mocks
	ctrl := gomock.NewController(t)
	valStore := mock.NewMockValidatorStore(ctrl)
	privkey := secp256k1.GenPrivKey()
	pubkey := privkey.PubKey()
	addr := sdk.AccAddress(pubkey.Address())
	tmPk := cmtprotocrypto.PublicKey{
		Sum: &cmtprotocrypto.PublicKey_Secp256K1{
			Secp256K1: pubkey.Bytes(),
		},
	}

	consAddr := sdk.ConsAddress(addr.String())
	valStore.EXPECT().GetPubKeyByConsAddr(gomock.Any(), consAddr.Bytes()).Return(tmPk, nil)

	// set up baseapp
	prepareOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			ctx = ctx.WithBlockHeight(req.Height).WithChainID(bapp.ChainID())
			_, info := extendedCommitToLastCommit(req.LocalLastCommit)
			ctx = ctx.WithCometInfo(info)
			err := baseapp.ValidateVoteExtensions(ctx, valStore, 0, "", req.LocalLastCommit)
			if err != nil {
				return nil, err
			}

			cp := ctx.ConsensusParams()
			extsEnabled := cp.Abci != nil && req.Height >= cp.Abci.VoteExtensionsEnableHeight && cp.Abci.VoteExtensionsEnableHeight != 0
			if extsEnabled {
				req.Txs = append(req.Txs, []byte("some-tx-that-does-something-from-votes"))
			}
			return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil
		})
	}

	suite := NewBaseAppSuite(t, prepareOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		InitialHeight: 1,
		ConsensusParams: &cmtproto.ConsensusParams{
			Abci: &cmtproto.ABCIParams{
				VoteExtensionsEnableHeight: 2,
			},
		},
	})
	require.NoError(t, err)

	// first test without vote extensions, no new txs should be added
	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1, // this value can't be 0
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 0, len(resPrepareProposal.Txs))

	// now we try with vote extensions, a new tx should show up
	marshalDelimitedFn := func(msg proto.Message) ([]byte, error) {
		var buf bytes.Buffer
		if err := protoio.NewDelimitedWriter(&buf).WriteMsg(msg); err != nil {
			return nil, err
		}

		return buf.Bytes(), nil
	}

	ext := []byte("something")
	cve := cmtproto.CanonicalVoteExtension{
		Extension: ext,
		Height:    2, // the vote extension was signed in the previous height
		Round:     int64(0),
		ChainId:   suite.baseApp.ChainID(),
	}

	bz, err := marshalDelimitedFn(&cve)
	require.NoError(t, err)

	extSig, err := privkey.Sign(bz)
	require.NoError(t, err)

	reqPrepareProposal = abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     3, // this value can't be 0
		LocalLastCommit: abci.ExtendedCommitInfo{
			Round: 0,
			Votes: []abci.ExtendedVoteInfo{
				{
					Validator: abci.Validator{
						Address: consAddr.Bytes(),
						Power:   666,
					},
					VoteExtension:      ext,
					ExtensionSignature: extSig,
					BlockIdFlag:        cmtproto.BlockIDFlagCommit,
				},
			},
		},
	}
	resPrepareProposal, err = suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 1, len(resPrepareProposal.Txs))

	// now vote extensions but our sole voter doesn't reach majority
	reqPrepareProposal = abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     3, // this value can't be 0
		LocalLastCommit: abci.ExtendedCommitInfo{
			Round: 0,
			Votes: []abci.ExtendedVoteInfo{
				{
					Validator: abci.Validator{
						Address: consAddr.Bytes(),
						Power:   666,
					},
					VoteExtension:      ext,
					ExtensionSignature: extSig,
					BlockIdFlag:        cmtproto.BlockIDFlagNil, // This will ignore the vote extension
				},
			},
		},
	}
	resPrepareProposal, err = suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 0, len(resPrepareProposal.Txs))
}

func TestABCI_ProcessProposal_PanicRecovery(t *testing.T) {
	processOpt := func(app *baseapp.BaseApp) {
		app.SetProcessProposal(func(ctx sdk.Context, rpp *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
			panic(errors.New("test"))
		})
	}
	suite := NewBaseAppSuite(t, processOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	require.NotPanics(t, func() {
		res, err := suite.baseApp.ProcessProposal(&abci.RequestProcessProposal{Height: 1})
		require.NoError(t, err)
		require.Equal(t, res.Status, abci.ResponseProcessProposal_REJECT)
	})
}

// TestABCI_Proposal_Reset_State ensures that state is reset between runs of
// PrepareProposal and ProcessProposal in case they are called multiple times.
// This is only valid for heights > 1, given that on height 1 we always set the
// state to be deliverState.
func TestABCI_Proposal_Reset_State_Between_Calls(t *testing.T) {
	someKey := []byte("some-key")

	prepareOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			// This key should not exist given that we reset the state on every call.
			require.False(t, ctx.KVStore(capKey1).Has(someKey))
			ctx.KVStore(capKey1).Set(someKey, someKey)
			return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil
		})
	}

	processOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetProcessProposal(func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
			// This key should not exist given that we reset the state on every call.
			require.False(t, ctx.KVStore(capKey1).Has(someKey))
			ctx.KVStore(capKey1).Set(someKey, someKey)
			return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
		})
	}

	suite := NewBaseAppSuite(t, prepareOpt, processOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     2, // this value can't be 0
	}

	// Let's pretend something happened and PrepareProposal gets called many
	// times, this must be safe to do.
	for range 5 {
		resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
		require.NoError(t, err)
		require.Equal(t, 0, len(resPrepareProposal.Txs))
	}

	reqProposalTxBytes := [][]byte{}
	reqProcessProposal := abci.RequestProcessProposal{
		Txs:    reqProposalTxBytes,
		Height: 2,
	}

	// Let's pretend something happened and ProcessProposal gets called many
	// times, this must be safe to do.
	for range 5 {
		resProcessProposal, err := suite.baseApp.ProcessProposal(&reqProcessProposal)
		require.NoError(t, err)
		require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)
	}
}

func TestABCI_HaltChain(t *testing.T) {
	testCases := []struct {
		name        string
		haltHeight  uint64
		haltTime    uint64
		blockHeight int64
		blockTime   int64
		expHalt     bool
	}{
		{"default", 0, 0, 10, 0, false},
		{"halt-height-edge", 11, 0, 10, 0, false},
		{"halt-height-equal", 10, 0, 10, 0, true},
		{"halt-height", 10, 0, 10, 0, true},
		{"halt-time-edge", 0, 11, 1, 10, false},
		{"halt-time-equal", 0, 10, 1, 10, true},
		{"halt-time", 0, 10, 1, 11, true},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			suite := NewBaseAppSuite(t, baseapp.SetHaltHeight(tc.haltHeight), baseapp.SetHaltTime(tc.haltTime))
			_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
				ConsensusParams: &cmtproto.ConsensusParams{},
				InitialHeight:   tc.blockHeight,
			})
			require.NoError(t, err)

			app := suite.baseApp
			_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{
				Height: tc.blockHeight,
				Time:   time.Unix(tc.blockTime, 0),
			})
			if !tc.expHalt {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.True(t, strings.HasPrefix(err.Error(), "halt per configuration"))
			}
		})
	}
}

func TestBaseApp_PreBlocker(t *testing.T) {
	db := dbm.NewMemDB()
	name := t.Name()
	logger := log.NewTestLogger(t)

	app := baseapp.NewBaseApp(name, logger, db, nil)
	_, err := app.InitChain(&abci.RequestInitChain{})
	require.NoError(t, err)

	wasHookCalled := false
	app.SetPreBlocker(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
		wasHookCalled = true

		ctx.EventManager().EmitEvent(sdk.NewEvent("preblockertest", sdk.NewAttribute("height", fmt.Sprintf("%d", req.Height))))
		return &sdk.ResponsePreBlock{ConsensusParamsChanged: false}, nil
	})
	app.Seal()

	res, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.NoError(t, err)
	require.Equal(t, true, wasHookCalled)
	require.Len(t, res.Events, 1)
	require.Equal(t, "preblockertest", res.Events[0].Type)

	// Now try erroring
	app = baseapp.NewBaseApp(name, logger, db, nil)
	_, err = app.InitChain(&abci.RequestInitChain{})
	require.NoError(t, err)

	app.SetPreBlocker(func(_ sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
		return nil, errors.New("some error")
	})
	app.Seal()

	_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1})
	require.Error(t, err)
}

// TestBaseApp_VoteExtensions tests vote extensions using a price as an example.
func TestBaseApp_VoteExtensions(t *testing.T) {
	ctrl := gomock.NewController(t)
	valStore := mock.NewMockValidatorStore(ctrl)

	// 10 good vote extensions, 2 bad ones from 12 total validators
	numVals := 12
	privKeys := make([]secp256k1.PrivKey, numVals)
	vals := make([]sdk.ConsAddress, numVals)
	for i := range numVals {
		privKey := secp256k1.GenPrivKey()
		privKeys[i] = privKey

		pubKey := privKey.PubKey()
		val := sdk.ConsAddress(pubKey.Bytes())
		vals[i] = val

		tmPk := cmtprotocrypto.PublicKey{
			Sum: &cmtprotocrypto.PublicKey_Secp256K1{
				Secp256K1: pubKey.Bytes(),
			},
		}
		valStore.EXPECT().GetPubKeyByConsAddr(gomock.Any(), val).Return(tmPk, nil)
	}

	baseappOpts := func(app *baseapp.BaseApp) {
		app.SetExtendVoteHandler(func(sdk.Context, *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
			// here we would have a process to get the price from an external source
			price := 10000000 + rand.Int63n(1000000)
			ve := make([]byte, 8)
			binary.BigEndian.PutUint64(ve, uint64(price))
			return &abci.ResponseExtendVote{VoteExtension: ve}, nil
		})

		app.SetVerifyVoteExtensionHandler(func(_ sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
			vePrice := binary.BigEndian.Uint64(req.VoteExtension)
			// here we would do some price validation, must not be 0 and not too high
			if vePrice > 11000000 || vePrice == 0 {
				// usually application should always return ACCEPT unless they really want to discard the entire vote
				return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, nil
			}

			return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil
		})

		app.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			txs := [][]byte{}
			ctx = ctx.WithBlockHeight(req.Height).WithChainID(app.ChainID())
			_, info := extendedCommitToLastCommit(req.LocalLastCommit)
			ctx = ctx.WithCometInfo(info)
			if err := baseapp.ValidateVoteExtensions(ctx, valStore, 0, "", req.LocalLastCommit); err != nil {
				return nil, err
			}
			// add all VE as txs (in a real scenario we would need to check signatures too)
			for _, v := range req.LocalLastCommit.Votes {
				if len(v.VoteExtension) == 8 {
					// pretend this is a way to check if the VE is valid
					if binary.BigEndian.Uint64(v.VoteExtension) < 11000000 && binary.BigEndian.Uint64(v.VoteExtension) > 0 {
						txs = append(txs, v.VoteExtension)
					}
				}
			}

			return &abci.ResponsePrepareProposal{Txs: txs}, nil
		})

		app.SetProcessProposal(func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
			// here we check if the proposal is valid, mainly if the vote extensions appended to the txs are valid
			for _, v := range req.Txs {
				// pretend this is a way to check if the tx is actually a VE
				if len(v) == 8 {
					// pretend this is a way to check if the VE is valid
					if binary.BigEndian.Uint64(v) > 11000000 || binary.BigEndian.Uint64(v) == 0 {
						return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
					}
				}
			}

			return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
		})

		app.SetPreBlocker(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
			count := uint64(0)
			pricesSum := uint64(0)
			for _, v := range req.Txs {
				// pretend this is a way to check if the tx is actually a VE
				if len(v) == 8 {
					count++
					pricesSum += binary.BigEndian.Uint64(v)
				}
			}

			if count > 0 {
				// we process the average price and store it in the context to make it available for FinalizeBlock
				avgPrice := pricesSum / count
				buf := make([]byte, 8)
				binary.BigEndian.PutUint64(buf, avgPrice)
				ctx.KVStore(capKey1).Set([]byte("avgPrice"), buf)
			}

			return &sdk.ResponsePreBlock{
				ConsensusParamsChanged: true,
			}, nil
		})
	}

	suite := NewBaseAppSuite(t, baseappOpts)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{
			Abci: &cmtproto.ABCIParams{
				VoteExtensionsEnableHeight: 1,
			},
		},
	})
	require.NoError(t, err)

	allVEs := [][]byte{}
	// simulate getting 10 vote extensions from 10 validators
	for range 10 {
		ve, err := suite.baseApp.ExtendVote(context.TODO(), &abci.RequestExtendVote{Height: 1})
		require.NoError(t, err)
		allVEs = append(allVEs, ve.VoteExtension)
	}

	// add a couple of invalid vote extensions (in what regards to the check we are doing in VerifyVoteExtension/ProcessProposal)
	// add a 0 price
	ve := make([]byte, 8)
	binary.BigEndian.PutUint64(ve, uint64(0))
	allVEs = append(allVEs, ve)

	// add a price too high
	ve = make([]byte, 8)
	binary.BigEndian.PutUint64(ve, uint64(13000000))
	allVEs = append(allVEs, ve)

	// verify all votes, only 10 should be accepted
	successful := 0
	for _, v := range allVEs {
		res, err := suite.baseApp.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{
			Height:        1,
			VoteExtension: v,
		})
		require.NoError(t, err)
		if res.Status == abci.ResponseVerifyVoteExtension_ACCEPT {
			successful++
		}
	}
	require.Equal(t, 10, successful)

	extVotes := []abci.ExtendedVoteInfo{}
	for _, val := range vals {
		extVotes = append(extVotes, abci.ExtendedVoteInfo{
			VoteExtension:      allVEs[0],
			BlockIdFlag:        cmtproto.BlockIDFlagCommit,
			ExtensionSignature: []byte{},
			Validator: abci.Validator{
				Address: val.Bytes(),
				Power:   666,
			},
		},
		)
	}

	prepPropReq := &abci.RequestPrepareProposal{
		Height: 1,
		LocalLastCommit: abci.ExtendedCommitInfo{
			Round: 0,
			Votes: extVotes,
		},
	}

	// add all VEs to the local last commit, which will make PrepareProposal fail
	// because it's not expecting to receive vote extensions when height == VoteExtensionsEnableHeight
	for _, ve := range allVEs {
		prepPropReq.LocalLastCommit.Votes = append(prepPropReq.LocalLastCommit.Votes, abci.ExtendedVoteInfo{
			VoteExtension:      ve,
			BlockIdFlag:        cmtproto.BlockIDFlagCommit,
			ExtensionSignature: []byte{}, // doesn't matter, it's just to make the next PrepareProposal fail
		})
	}
	resp, err := suite.baseApp.PrepareProposal(prepPropReq)
	require.Len(t, resp.Txs, 0) // this is actually a failure, but we don't want to halt the chain
	require.NoError(t, err)     // we don't error here

	prepPropReq.LocalLastCommit.Votes = []abci.ExtendedVoteInfo{} // reset votes
	resp, err = suite.baseApp.PrepareProposal(prepPropReq)
	require.NoError(t, err)
	require.Len(t, resp.Txs, 0)

	procPropRes, err := suite.baseApp.ProcessProposal(&abci.RequestProcessProposal{Height: 1, Txs: resp.Txs})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, procPropRes.Status)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1, Txs: resp.Txs})
	require.NoError(t, err)

	// The average price will be nil during the first block, given that we don't have
	// any vote extensions on block 1 in PrepareProposal
	avgPrice := getFinalizeBlockStateCtx(suite.baseApp).KVStore(capKey1).Get([]byte("avgPrice"))
	require.Nil(t, avgPrice)
	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	// Now onto the second block, this time we process vote extensions from the
	// previous block (which we sign now)
	for i, ve := range allVEs {
		cve := cmtproto.CanonicalVoteExtension{
			Extension: ve,
			Height:    1,
			Round:     int64(0),
			ChainId:   suite.baseApp.ChainID(),
		}

		bz, err := marshalDelimitedFn(&cve)
		require.NoError(t, err)

		privKey := privKeys[i]
		extSig, err := privKey.Sign(bz)
		require.NoError(t, err)

		prepPropReq.LocalLastCommit.Votes = append(prepPropReq.LocalLastCommit.Votes, abci.ExtendedVoteInfo{
			VoteExtension:      ve,
			BlockIdFlag:        cmtproto.BlockIDFlagCommit,
			ExtensionSignature: extSig,
			Validator: abci.Validator{
				Address: vals[i].Bytes(),
				Power:   666,
			},
		})
	}

	prepPropReq.Height = 2
	resp, err = suite.baseApp.PrepareProposal(prepPropReq)
	require.NoError(t, err)
	require.Len(t, resp.Txs, 10)

	procPropRes, err = suite.baseApp.ProcessProposal(&abci.RequestProcessProposal{Height: 2, Txs: resp.Txs})
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, procPropRes.Status)

	_, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 2, Txs: resp.Txs})
	require.NoError(t, err)

	// Check if the average price was available in FinalizeBlock's context
	avgPrice = getFinalizeBlockStateCtx(suite.baseApp).KVStore(capKey1).Get([]byte("avgPrice"))
	require.NotNil(t, avgPrice)
	require.GreaterOrEqual(t, binary.BigEndian.Uint64(avgPrice), uint64(10000000))
	require.Less(t, binary.BigEndian.Uint64(avgPrice), uint64(11000000))

	_, err = suite.baseApp.Commit()
	require.NoError(t, err)

	// check if avgPrice was committed
	committedAvgPrice := suite.baseApp.NewContext(true).KVStore(capKey1).Get([]byte("avgPrice"))
	require.Equal(t, avgPrice, committedAvgPrice)
}

func TestABCI_PrepareProposal_Panic(t *testing.T) {
	prepareOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetPrepareProposal(func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
			if len(req.Txs) == 3 {
				panic("i don't like number 3, panic")
			}
			// return empty if no panic
			return &abci.ResponsePrepareProposal{}, nil
		})
	}

	suite := NewBaseAppSuite(t, prepareOpt)

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		InitialHeight:   1,
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	txs := [][]byte{{1}, {2}}
	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1, // this value can't be 0
		Txs:        txs,
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 0, len(resPrepareProposal.Txs))

	// make it panic, and check if it returns 3 txs (because of panic recovery)
	txs = [][]byte{{1}, {2}, {3}}
	reqPrepareProposal.Txs = txs
	resPrepareProposal, err = suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 3, len(resPrepareProposal.Txs))
}

func TestOptimisticExecution(t *testing.T) {
	suite := NewBaseAppSuite(t, baseapp.SetOptimisticExecution())

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	// run 50 blocks
	for range 50 {
		tx := newTxCounter(t, suite.txConfig, 0, 1)
		txBytes, err := suite.txConfig.TxEncoder()(tx)
		require.NoError(t, err)

		reqProcProp := abci.RequestProcessProposal{
			Txs:    [][]byte{txBytes},
			Height: suite.baseApp.LastBlockHeight() + 1,
			Hash:   []byte("some-hash" + strconv.FormatInt(suite.baseApp.LastBlockHeight()+1, 10)),
		}

		respProcProp, err := suite.baseApp.ProcessProposal(&reqProcProp)
		require.Equal(t, abci.ResponseProcessProposal_ACCEPT, respProcProp.Status)
		require.NoError(t, err)

		reqFinalizeBlock := abci.RequestFinalizeBlock{
			Height: reqProcProp.Height,
			Txs:    reqProcProp.Txs,
			Hash:   reqProcProp.Hash,
		}

		respFinalizeBlock, err := suite.baseApp.FinalizeBlock(&reqFinalizeBlock)
		require.NoError(t, err)
		require.Len(t, respFinalizeBlock.TxResults, 1)

		_, err = suite.baseApp.Commit()
		require.NoError(t, err)
	}

	require.Equal(t, int64(50), suite.baseApp.LastBlockHeight())
}

func TestABCI_Proposal_FailReCheckTx(t *testing.T) {
	pool := mempool.NewPriorityMempool[int64](mempool.PriorityNonceMempoolConfig[int64]{
		TxPriority:      mempool.NewDefaultTxPriority(),
		MaxTx:           0,
		SignerExtractor: mempool.NewDefaultSignerExtractionAdapter(),
	})

	anteOpt := func(bapp *baseapp.BaseApp) {
		bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
			// always fail on recheck, just to test the recheck logic
			if ctx.IsReCheckTx() {
				return ctx, errors.New("recheck failed in ante handler")
			}

			return ctx, nil
		})
	}

	suite := NewBaseAppSuite(t, anteOpt, baseapp.SetMempool(pool))
	baseapptestutil.RegisterKeyValueServer(suite.baseApp.MsgServiceRouter(), MsgKeyValueImpl{})
	baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), NoopCounterServerImpl{})

	_, err := suite.baseApp.InitChain(&abci.RequestInitChain{
		ConsensusParams: &cmtproto.ConsensusParams{},
	})
	require.NoError(t, err)

	tx := newTxCounter(t, suite.txConfig, 0, 1)
	txBytes, err := suite.txConfig.TxEncoder()(tx)
	require.NoError(t, err)

	reqCheckTx := abci.RequestCheckTx{
		Tx:   txBytes,
		Type: abci.CheckTxType_New,
	}
	_, err = suite.baseApp.CheckTx(&reqCheckTx)
	require.NoError(t, err)

	tx2 := newTxCounter(t, suite.txConfig, 1, 1)

	tx2Bytes, err := suite.txConfig.TxEncoder()(tx2)
	require.NoError(t, err)

	err = pool.Insert(sdk.Context{}, tx2)
	require.NoError(t, err)

	require.Equal(t, 2, pool.CountTx())

	// call prepareProposal before calling recheck tx, just as a sanity check
	reqPrepareProposal := abci.RequestPrepareProposal{
		MaxTxBytes: 1000,
		Height:     1,
	}
	resPrepareProposal, err := suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 2, len(resPrepareProposal.Txs))

	// call recheck on the first tx, it MUST return an error
	reqReCheckTx := abci.RequestCheckTx{
		Tx:   txBytes,
		Type: abci.CheckTxType_Recheck,
	}
	resp, err := suite.baseApp.CheckTx(&reqReCheckTx)
	require.NoError(t, err)
	require.True(t, resp.IsErr())
	require.Equal(t, "recheck failed in ante handler", resp.Log)

	// call prepareProposal again, should return only the second tx
	resPrepareProposal, err = suite.baseApp.PrepareProposal(&reqPrepareProposal)
	require.NoError(t, err)
	require.Equal(t, 1, len(resPrepareProposal.Txs))
	require.Equal(t, tx2Bytes, resPrepareProposal.Txs[0])

	// check the mempool, it should have only the second tx
	require.Equal(t, 1, pool.CountTx())

	reqProposalTxBytes := [][]byte{
		tx2Bytes,
	}
	reqProcessProposal := abci.RequestProcessProposal{
		Txs:    reqProposalTxBytes,
		Height: reqPrepareProposal.Height,
	}

	resProcessProposal, err := suite.baseApp.ProcessProposal(&reqProcessProposal)
	require.NoError(t, err)
	require.Equal(t, abci.ResponseProcessProposal_ACCEPT, resProcessProposal.Status)

	// the same txs as in PrepareProposal
	res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: suite.baseApp.LastBlockHeight() + 1,
		Txs:    reqProposalTxBytes,
	})
	require.NoError(t, err)

	require.Equal(t, 0, pool.CountTx())

	require.NotEmpty(t, res.TxResults[0].Events)
	require.True(t, res.TxResults[0].IsOK(), fmt.Sprintf("%v", res))
}

func TestFinalizeBlockDeferResponseHandle(t *testing.T) {
	suite := NewBaseAppSuite(t, baseapp.SetHaltHeight(1), func(ba *baseapp.BaseApp) {
		ba.SetStreamingManager(storetypes.StreamingManager{
			ABCIListeners: []storetypes.ABCIListener{
				&mockABCIListener{},
			},
		})
	})

	res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{
		Height: 2,
	})
	require.Empty(t, res)
	require.NotEmpty(t, err)
}
