From cee9de30e3b1797b5105bead91f37593e6415f3c Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 26 Sep 2024 15:16:28 +0200 Subject: [PATCH] ci: actually enable v2 system test (#21539) Co-authored-by: Matt Kocubinski (cherry picked from commit f927e9b5517327c04398bba76a73b2f9f072227e) # Conflicts: # core/server/app.go # server/v2/cometbft/go.mod # server/v2/cometbft/go.sum # server/v2/stf/stf.go --- .github/workflows/test.yml | 5 +- Makefile | 2 +- client/grpc/cmtservice/status_test.go | 2 +- client/v2/autocli/query_test.go | 2 - core/server/app.go | 55 ++ scripts/local-testnet.sh | 17 - server/v2/cometbft/abci.go | 24 +- server/v2/cometbft/commands.go | 14 +- server/v2/cometbft/go.mod | 18 +- server/v2/cometbft/go.sum | 7 + server/v2/cometbft/query.go | 2 +- server/v2/cometbft/streaming.go | 8 +- server/v2/cometbft/types/errors/errors.go | 2 +- server/v2/cometbft/utils.go | 35 +- server/v2/stf/stf.go | 600 ++++++++++++++++++++++ tests/systemtests/testnet_init.go | 1 - tests/systemtests/unordered_tx_test.go | 3 +- x/auth/ante/basic_test.go | 2 +- 18 files changed, 723 insertions(+), 76 deletions(-) create mode 100644 core/server/app.go delete mode 100644 scripts/local-testnet.sh create mode 100644 server/v2/stf/stf.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cf55607bc7f..20ebc1f5883e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -145,7 +145,6 @@ jobs: path: ./tests/e2e-profile.out test-system: - needs: [tests, test-integration, test-e2e] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -177,7 +176,7 @@ jobs: - name: system tests v1 if: env.GIT_DIFF run: | - COSMOS_BUILD_OPTIONS=legacy make test-system + make test-system - uses: actions/upload-artifact@v3 if: failure() with: @@ -187,7 +186,7 @@ jobs: - name: system tests v2 if: env.GIT_DIFF run: | - make test-system + COSMOS_BUILD_OPTIONS=v2 make test-system - uses: actions/upload-artifact@v3 if: failure() with: diff --git a/Makefile b/Makefile index 067b5d4f6103..775dc0cd3d7c 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include scripts/build/build.mk .DEFAULT_GOAL := help -#? go.sum: Run go mod tidy and ensure dependencies have not been modified +#? go.sum: Run go mod tidy and ensure dependencies have not been modified. go.sum: go.mod echo "Ensure dependencies have not been modified ..." >&2 go mod verify diff --git a/client/grpc/cmtservice/status_test.go b/client/grpc/cmtservice/status_test.go index 9f7ab9567921..d7bfa6bdc192 100644 --- a/client/grpc/cmtservice/status_test.go +++ b/client/grpc/cmtservice/status_test.go @@ -14,7 +14,7 @@ import ( ) func TestStatusCommand(t *testing.T) { - t.Skip() // https://github.com/cosmos/cosmos-sdk/issues/17446 + t.Skip() // Rewrite as system test cfg, err := network.DefaultConfigWithAppConfig(depinject.Configs() /* TODO, test skipped anyway */) require.NoError(t, err) diff --git a/client/v2/autocli/query_test.go b/client/v2/autocli/query_test.go index f5db25c40b36..1cbca64fb951 100644 --- a/client/v2/autocli/query_test.go +++ b/client/v2/autocli/query_test.go @@ -575,8 +575,6 @@ func TestBinaryFlag(t *testing.T) { } func TestAddressValidation(t *testing.T) { - t.Skip() // TODO(@julienrbrt) re-able with better keyring instiantiation - fixture := initFixture(t) _, err := runCmd(fixture, buildModuleQueryCommand, diff --git a/core/server/app.go b/core/server/app.go new file mode 100644 index 000000000000..56a0c0862777 --- /dev/null +++ b/core/server/app.go @@ -0,0 +1,55 @@ +package server + +import ( + "context" + "time" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/event" + "cosmossdk.io/core/transaction" +) + +// BlockRequest defines the request structure for a block coming from consensus server to the state transition function. +type BlockRequest[T transaction.Tx] struct { + Height uint64 + Time time.Time + Hash []byte + ChainId string + AppHash []byte + Txs []T + + // IsGenesis indicates if this block is the first block of the chain. + IsGenesis bool +} + +// BlockResponse defines the response structure for a block coming from the state transition function to consensus server. +type BlockResponse struct { + ValidatorUpdates []appmodulev2.ValidatorUpdate + PreBlockEvents []event.Event + BeginBlockEvents []event.Event + TxResults []TxResult + EndBlockEvents []event.Event +} + +// TxResult defines the result of a transaction execution. +type TxResult struct { + // Events produced by the transaction. + Events []event.Event + // Response messages produced by the transaction. + Resp []transaction.Msg + // Error produced by the transaction. + Error error + // GasWanted is the maximum units of work we allow this tx to perform. + GasWanted uint64 + // GasUsed is the amount of gas actually consumed. + GasUsed uint64 +} + +// VersionModifier defines the interface fulfilled by BaseApp +// which allows getting and setting its appVersion field. This +// in turn updates the consensus params that are sent to the +// consensus engine in EndBlock +type VersionModifier interface { + SetAppVersion(context.Context, uint64) error + AppVersion(context.Context) (uint64, error) +} diff --git a/scripts/local-testnet.sh b/scripts/local-testnet.sh deleted file mode 100644 index c73710dbab1e..000000000000 --- a/scripts/local-testnet.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset -set -x - -ROOT=$PWD - -SIMD="$ROOT/build/simdv2" - -COSMOS_BUILD_OPTIONS=v2 make build - -$SIMD testnet init-files --chain-id=testing --output-dir="$HOME/.testnet" --validator-count=3 --keyring-backend=test --minimum-gas-prices=0.000001stake --commit-timeout=900ms --single-host - -$SIMD start --log_level=info --home "$HOME/.testnet/node0/simdv2" & -$SIMD start --log_level=info --home "$HOME/.testnet/node1/simdv2" & -$SIMD start --log_level=info --home "$HOME/.testnet/node2/simdv2" \ No newline at end of file diff --git a/server/v2/cometbft/abci.go b/server/v2/cometbft/abci.go index 537b3e2dc5f5..85b38e4ccc65 100644 --- a/server/v2/cometbft/abci.go +++ b/server/v2/cometbft/abci.go @@ -17,7 +17,7 @@ import ( "cosmossdk.io/core/server" "cosmossdk.io/core/store" "cosmossdk.io/core/transaction" - errorsmod "cosmossdk.io/errors" + errorsmod "cosmossdk.io/errors/v2" "cosmossdk.io/log" "cosmossdk.io/server/v2/appmanager" "cosmossdk.io/server/v2/cometbft/client/grpc/cmtservice" @@ -122,7 +122,8 @@ func (c *Consensus[T]) CheckTx(ctx context.Context, req *abciproto.CheckTxReques } resp, err := c.app.ValidateTx(ctx, decodedTx) - if err != nil { + // we do not want to return a cometbft error, but a check tx response with the error + if err != nil && err != resp.Error { return nil, err } @@ -132,15 +133,18 @@ func (c *Consensus[T]) CheckTx(ctx context.Context, req *abciproto.CheckTxReques } cometResp := &abciproto.CheckTxResponse{ - Code: resp.Code, + Code: 0, GasWanted: uint64ToInt64(resp.GasWanted), GasUsed: uint64ToInt64(resp.GasUsed), Events: events, } if resp.Error != nil { - cometResp.Code = 1 - cometResp.Log = resp.Error.Error() + space, code, log := errorsmod.ABCIInfo(resp.Error, c.cfg.AppTomlConfig.Trace) + cometResp.Code = code + cometResp.Codespace = space + cometResp.Log = log } + return cometResp, nil } @@ -196,7 +200,7 @@ func (c *Consensus[T]) Query(ctx context.Context, req *abciproto.QueryRequest) ( } res, err := c.app.Query(ctx, uint64(req.Height), protoRequest) if err != nil { - resp := queryResult(err) + resp := QueryResult(err, c.cfg.AppTomlConfig.Trace) resp.Height = req.Height return resp, err @@ -283,10 +287,10 @@ func (c *Consensus[T]) InitChain(ctx context.Context, req *abciproto.InitChainRe return nil, fmt.Errorf("genesis state init failure: %w", err) } - // TODO necessary? where should this WARN live if it all. helpful for testing for _, txRes := range blockresponse.TxResults { - if txRes.Error != nil { - c.logger.Warn("genesis tx failed", "code", txRes.Code, "error", txRes.Error) + if err := txRes.Error; err != nil { + space, code, log := errorsmod.ABCIInfo(err, c.cfg.AppTomlConfig.Trace) + c.logger.Warn("genesis tx failed", "codespace", space, "code", code, "log", log) } } @@ -485,7 +489,7 @@ func (c *Consensus[T]) FinalizeBlock( return nil, err } - return finalizeBlockResponse(resp, cp, appHash, c.indexedEvents) + return finalizeBlockResponse(resp, cp, appHash, c.indexedEvents, c.cfg.AppTomlConfig.Trace) } // Commit implements types.Application. diff --git a/server/v2/cometbft/commands.go b/server/v2/cometbft/commands.go index 367700e98269..ff4ddb01c41f 100644 --- a/server/v2/cometbft/commands.go +++ b/server/v2/cometbft/commands.go @@ -106,15 +106,13 @@ func ShowValidatorCmd() *cobra.Command { return err } - cmd.Println(sdkPK) // TODO: figure out if we need the codec here or not, see below - - // clientCtx := client.GetClientContextFromCmd(cmd) - // bz, err := clientCtx.Codec.MarshalInterfaceJSON(sdkPK) - // if err != nil { - // return err - // } + clientCtx := client.GetClientContextFromCmd(cmd) + bz, err := clientCtx.Codec.MarshalInterfaceJSON(sdkPK) + if err != nil { + return err + } - // cmd.Println(string(bz)) + cmd.Println(string(bz)) return nil }, } diff --git a/server/v2/cometbft/go.mod b/server/v2/cometbft/go.mod index 7c08b2e890ee..ff91e3db9dfb 100644 --- a/server/v2/cometbft/go.mod +++ b/server/v2/cometbft/go.mod @@ -3,12 +3,22 @@ module cosmossdk.io/server/v2/cometbft go 1.23.1 replace ( +<<<<<<< HEAD // pseudo version lower than the latest tag cosmossdk.io/api => cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf // main // pseudo version lower than the latest tag cosmossdk.io/core => cosmossdk.io/core v1.0.0-alpha.3 // main // pseudo version lower than the latest tag cosmossdk.io/store => cosmossdk.io/store v1.0.0-rc.0.0.20240913190136-3bc707a5a214 // main +======= + cosmossdk.io/api => ../../../api + cosmossdk.io/core => ../../../core + cosmossdk.io/server/v2 => ../ + cosmossdk.io/server/v2/appmanager => ../appmanager + cosmossdk.io/server/v2/stf => ../stf + cosmossdk.io/store => ../../../store + cosmossdk.io/store/v2 => ../../../store/v2 +>>>>>>> f927e9b55 (ci: actually enable v2 system test (#21539)) cosmossdk.io/x/bank => ../../../x/bank cosmossdk.io/x/consensus => ../../../x/consensus cosmossdk.io/x/staking => ../../../x/staking @@ -16,9 +26,15 @@ replace ( ) require ( +<<<<<<< HEAD cosmossdk.io/api v0.8.0 cosmossdk.io/core v1.0.0 // main cosmossdk.io/errors v1.0.1 +======= + cosmossdk.io/api v0.7.6 + cosmossdk.io/core v1.0.0-alpha.3 + cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 +>>>>>>> f927e9b55 (ci: actually enable v2 system test (#21539)) cosmossdk.io/log v1.4.1 cosmossdk.io/server/v2 v2.0.0-20240920095614-aa90bb43d8f8 // main cosmossdk.io/server/v2/appmanager v0.0.0-20240920095614-aa90bb43d8f8 // main @@ -42,7 +58,7 @@ require ( cosmossdk.io/collections v0.4.1-0.20240802064046-23fac2f1b8ab // indirect cosmossdk.io/core/testing v0.0.0-20240913164418-aaf72f20c10b // indirect cosmossdk.io/depinject v1.0.0 // indirect - cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect + cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/math v1.3.0 // indirect cosmossdk.io/schema v0.3.0 // indirect cosmossdk.io/store v1.1.1-0.20240909133312-50288938d1b6 // indirect diff --git a/server/v2/cometbft/go.sum b/server/v2/cometbft/go.sum index 3c9ef21716b4..e2b52cb00dd9 100644 --- a/server/v2/cometbft/go.sum +++ b/server/v2/cometbft/go.sum @@ -4,6 +4,7 @@ buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.34.2-20240130113600-88e buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.34.2-20240130113600-88ef6483f90f.2/go.mod h1:HqcXMSa5qnNuakaMUo+hWhF51mKbcrZxGl9Vp5EeJXc= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +<<<<<<< HEAD cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf h1:CttA/mEIxGm4E7vwrjUpju7/Iespns08d9bOza70cIc= cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf/go.mod h1:YMfx2ATpgITsoydD3hIBa8IkDHtyXp/14rmG0d3sEew= cosmossdk.io/collections v0.4.1-0.20240802064046-23fac2f1b8ab h1:E/IWad76v1Nc4Atswaccpt7twJ0VwHkbY94/PhmZfTo= @@ -12,6 +13,12 @@ cosmossdk.io/core v1.0.0-alpha.3 h1:pnxaYAas7llXgVz1lM7X6De74nWrhNKnB3yMKe4OUUA= cosmossdk.io/core v1.0.0-alpha.3/go.mod h1:3u9cWq1FAVtiiCrDPpo4LhR+9V6k/ycSG4/Y/tREWCY= cosmossdk.io/core/testing v0.0.0-20240913164418-aaf72f20c10b h1:uEMbr7Hdpz0fU+GXU6mSN2vgoQnr66WYUpRuiba2aEk= cosmossdk.io/core/testing v0.0.0-20240913164418-aaf72f20c10b/go.mod h1:FllCSj/ZYskfb982HKqBSISO8DkBY4Euqq768HSFz68= +======= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 h1:NxxUo0GMJUbIuVg0R70e3cbn9eFTEuMr7ev1AFvypdY= +cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29/go.mod h1:8s2tPeJtSiQuoyPmr2Ag7meikonISO4Fv4MoO8+ORrs= +>>>>>>> f927e9b55 (ci: actually enable v2 system test (#21539)) cosmossdk.io/depinject v1.0.0 h1:dQaTu6+O6askNXO06+jyeUAnF2/ssKwrrszP9t5q050= cosmossdk.io/depinject v1.0.0/go.mod h1:zxK/h3HgHoA/eJVtiSsoaRaRA2D5U4cJ5thIG4ssbB8= cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= diff --git a/server/v2/cometbft/query.go b/server/v2/cometbft/query.go index 912338ff79f3..8a624f56fc32 100644 --- a/server/v2/cometbft/query.go +++ b/server/v2/cometbft/query.go @@ -7,7 +7,7 @@ import ( abci "github.com/cometbft/cometbft/api/cometbft/abci/v1" crypto "github.com/cometbft/cometbft/api/cometbft/crypto/v1" - errorsmod "cosmossdk.io/errors" + errorsmod "cosmossdk.io/errors/v2" "cosmossdk.io/server/v2/cometbft/types" cometerrors "cosmossdk.io/server/v2/cometbft/types/errors" ) diff --git a/server/v2/cometbft/streaming.go b/server/v2/cometbft/streaming.go index 54abe20aaa96..2ccb33dffb99 100644 --- a/server/v2/cometbft/streaming.go +++ b/server/v2/cometbft/streaming.go @@ -6,6 +6,7 @@ import ( "cosmossdk.io/core/event" "cosmossdk.io/core/server" "cosmossdk.io/core/store" + errorsmod "cosmossdk.io/errors/v2" "cosmossdk.io/server/v2/streaming" ) @@ -21,12 +22,17 @@ func (c *Consensus[T]) streamDeliverBlockChanges( // convert txresults to streaming txresults streamingTxResults := make([]*streaming.ExecTxResult, len(txResults)) for i, txResult := range txResults { + space, code, log := errorsmod.ABCIInfo(txResult.Error, c.cfg.AppTomlConfig.Trace) + events, err := streaming.IntoStreamingEvents(txResult.Events) if err != nil { return err } + streamingTxResults[i] = &streaming.ExecTxResult{ - Code: txResult.Code, + Code: code, + Codespace: space, + Log: log, GasWanted: uint64ToInt64(txResult.GasWanted), GasUsed: uint64ToInt64(txResult.GasUsed), Events: events, diff --git a/server/v2/cometbft/types/errors/errors.go b/server/v2/cometbft/types/errors/errors.go index 380c176ff306..e60f32338358 100644 --- a/server/v2/cometbft/types/errors/errors.go +++ b/server/v2/cometbft/types/errors/errors.go @@ -1,7 +1,7 @@ package errors import ( - errorsmod "cosmossdk.io/errors" + errorsmod "cosmossdk.io/errors/v2" ) // RootCodespace is the codespace for all errors defined in this package diff --git a/server/v2/cometbft/utils.go b/server/v2/cometbft/utils.go index d48147e0a243..2b1052bd1953 100644 --- a/server/v2/cometbft/utils.go +++ b/server/v2/cometbft/utils.go @@ -18,7 +18,7 @@ import ( "cosmossdk.io/core/event" "cosmossdk.io/core/server" "cosmossdk.io/core/transaction" - errorsmod "cosmossdk.io/errors" + errorsmod "cosmossdk.io/errors/v2" consensus "cosmossdk.io/x/consensus/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -70,6 +70,7 @@ func finalizeBlockResponse( cp *cmtproto.ConsensusParams, appHash []byte, indexSet map[string]struct{}, + debug bool, ) (*abci.FinalizeBlockResponse, error) { allEvents := append(in.BeginBlockEvents, in.EndBlockEvents...) @@ -78,7 +79,7 @@ func finalizeBlockResponse( return nil, err } - txResults, err := intoABCITxResults(in.TxResults, indexSet) + txResults, err := intoABCITxResults(in.TxResults, indexSet, debug) if err != nil { return nil, err } @@ -107,29 +108,20 @@ func intoABCIValidatorUpdates(updates []appmodulev2.ValidatorUpdate) []abci.Vali return valsetUpdates } -func intoABCITxResults(results []server.TxResult, indexSet map[string]struct{}) ([]*abci.ExecTxResult, error) { +func intoABCITxResults(results []server.TxResult, indexSet map[string]struct{}, debug bool) ([]*abci.ExecTxResult, error) { res := make([]*abci.ExecTxResult, len(results)) for i := range results { - if results[i].Error != nil { - space, code, log := errorsmod.ABCIInfo(results[i].Error, true) - res[i] = &abci.ExecTxResult{ - Codespace: space, - Code: code, - Log: log, - } - - continue - } events, err := intoABCIEvents(results[i].Events, indexSet) if err != nil { return nil, err } + res[i] = responseExecTxResultWithEvents( results[i].Error, results[i].GasWanted, results[i].GasUsed, events, - false, + debug, ) } @@ -387,10 +379,10 @@ func (c *Consensus[T]) GetBlockRetentionHeight(cp *cmtproto.ConsensusParams, com func (c *Consensus[T]) checkHalt(height int64, time time.Time) error { var halt bool switch { - case c.cfg.AppTomlConfig.HaltHeight > 0 && uint64(height) > c.cfg.AppTomlConfig.HaltHeight: + case c.cfg.AppTomlConfig.HaltHeight > 0 && uint64(height) >= c.cfg.AppTomlConfig.HaltHeight: halt = true - case c.cfg.AppTomlConfig.HaltTime > 0 && time.Unix() > int64(c.cfg.AppTomlConfig.HaltTime): + case c.cfg.AppTomlConfig.HaltTime > 0 && time.Unix() >= int64(c.cfg.AppTomlConfig.HaltTime): halt = true } @@ -408,14 +400,3 @@ func uint64ToInt64(u uint64) int64 { } return int64(u) } - -// queryResult returns a ResponseQuery from an error. It will try to parse ABCI -// info from the error. -func queryResult(err error) *abci.QueryResponse { - space, code, log := errorsmod.ABCIInfo(err, false) - return &abci.QueryResponse{ - Codespace: space, - Code: code, - Log: log, - } -} diff --git a/server/v2/stf/stf.go b/server/v2/stf/stf.go new file mode 100644 index 000000000000..f8d69e972712 --- /dev/null +++ b/server/v2/stf/stf.go @@ -0,0 +1,600 @@ +package stf + +import ( + "context" + "errors" + "fmt" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/event" + "cosmossdk.io/core/gas" + "cosmossdk.io/core/header" + "cosmossdk.io/core/log" + "cosmossdk.io/core/router" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/schema/appdata" + stfgas "cosmossdk.io/server/v2/stf/gas" + "cosmossdk.io/server/v2/stf/internal" +) + +type eContextKey struct{} + +var executionContextKey = eContextKey{} + +// STF is a struct that manages the state transition component of the app. +type STF[T transaction.Tx] struct { + logger log.Logger + + msgRouter coreRouterImpl + queryRouter coreRouterImpl + + doPreBlock func(ctx context.Context, txs []T) error + doBeginBlock func(ctx context.Context) error + doEndBlock func(ctx context.Context) error + doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) + + doTxValidation func(ctx context.Context, tx T) error + postTxExec func(ctx context.Context, tx T, success bool) error + + branchFn branchFn // branchFn is a function that given a readonly state it returns a writable version of it. + makeGasMeter makeGasMeterFn + makeGasMeteredState makeGasMeteredStateFn +} + +// NewSTF returns a new STF instance. +func NewSTF[T transaction.Tx]( + logger log.Logger, + msgRouterBuilder *MsgRouterBuilder, + queryRouterBuilder *MsgRouterBuilder, + doPreBlock func(ctx context.Context, txs []T) error, + doBeginBlock func(ctx context.Context) error, + doEndBlock func(ctx context.Context) error, + doTxValidation func(ctx context.Context, tx T) error, + doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error), + postTxExec func(ctx context.Context, tx T, success bool) error, + branch func(store store.ReaderMap) store.WriterMap, +) (*STF[T], error) { + msgRouter, err := msgRouterBuilder.Build() + if err != nil { + return nil, fmt.Errorf("build msg router: %w", err) + } + queryRouter, err := queryRouterBuilder.Build() + if err != nil { + return nil, fmt.Errorf("build query router: %w", err) + } + + return &STF[T]{ + logger: logger, + msgRouter: msgRouter, + queryRouter: queryRouter, + doPreBlock: doPreBlock, + doBeginBlock: doBeginBlock, + doEndBlock: doEndBlock, + doValidatorUpdate: doValidatorUpdate, + doTxValidation: doTxValidation, + postTxExec: postTxExec, // TODO + branchFn: branch, + makeGasMeter: stfgas.DefaultGasMeter, + makeGasMeteredState: stfgas.DefaultWrapWithGasMeter, + }, nil +} + +// DeliverBlock is our state transition function. +// It takes a read only view of the state to apply the block to, +// executes the block and returns the block results and the new state. +func (s STF[T]) DeliverBlock( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, +) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { + // creates a new branchFn state, from the readonly view of the state + // that can be written to. + newState = s.branchFn(state) + hi := header.Info{ + Hash: block.Hash, + AppHash: block.AppHash, + ChainID: block.ChainId, + Time: block.Time, + Height: int64(block.Height), + } + // set header info + err = s.setHeaderInfo(newState, hi) + if err != nil { + return nil, nil, fmt.Errorf("unable to set initial header info, %w", err) + } + + exCtx := s.makeContext(ctx, ConsensusIdentity, newState, internal.ExecModeFinalize) + exCtx.setHeaderInfo(hi) + + // reset events + exCtx.events = make([]event.Event, 0) + // pre block is called separate from begin block in order to prepopulate state + preBlockEvents, err := s.preBlock(exCtx, block.Txs) + if err != nil { + return nil, nil, err + } + + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + + // reset events + exCtx.events = make([]event.Event, 0) + + // begin block + var beginBlockEvents []event.Event + if !block.IsGenesis { + // begin block + beginBlockEvents, err = s.beginBlock(exCtx) + if err != nil { + return nil, nil, err + } + } + + // check if we need to return early + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + + // execute txs + txResults := make([]server.TxResult, len(block.Txs)) + // TODO: skip first tx if vote extensions are enabled (marko) + for i, txBytes := range block.Txs { + // check if we need to return early or continue delivering txs + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi) + } + // reset events + exCtx.events = make([]event.Event, 0) + // end block + endBlockEvents, valset, err := s.endBlock(exCtx) + if err != nil { + return nil, nil, err + } + + return &server.BlockResponse{ + ValidatorUpdates: valset, + PreBlockEvents: preBlockEvents, + BeginBlockEvents: beginBlockEvents, + TxResults: txResults, + EndBlockEvents: endBlockEvents, + }, newState, nil +} + +// deliverTx executes a TX and returns the result. +func (s STF[T]) deliverTx( + ctx context.Context, + state store.WriterMap, + tx T, + execMode transaction.ExecMode, + hi header.Info, +) server.TxResult { + // recover in the case of a panic + var recoveryError error + defer func() { + if r := recover(); r != nil { + recoveryError = fmt.Errorf("panic during transaction execution: %s", r) + s.logger.Error("panic during transaction execution", "error", recoveryError) + } + }() + // handle error from GetGasLimit + gasLimit, gasLimitErr := tx.GetGasLimit() + if gasLimitErr != nil { + return server.TxResult{ + Error: gasLimitErr, + } + } + + if recoveryError != nil { + return server.TxResult{ + Error: recoveryError, + } + } + + validateGas, validationEvents, err := s.validateTx(ctx, state, gasLimit, tx, execMode) + if err != nil { + return server.TxResult{ + Error: err, + } + } + + execResp, execGas, execEvents, err := s.execTx(ctx, state, gasLimit-validateGas, tx, execMode, hi) + return server.TxResult{ + Events: append(validationEvents, execEvents...), + GasUsed: execGas + validateGas, + GasWanted: gasLimit, + Resp: execResp, + Error: err, + } +} + +// validateTx validates a transaction given the provided WritableState and gas limit. +// If the validation is successful, state is committed +func (s STF[T]) validateTx( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, +) (gasUsed uint64, events []event.Event, err error) { + validateState := s.branchFn(state) + hi, err := s.getHeaderInfo(validateState) + if err != nil { + return 0, nil, err + } + validateCtx := s.makeContext(ctx, RuntimeIdentity, validateState, execMode) + validateCtx.setHeaderInfo(hi) + validateCtx.setGasLimit(gasLimit) + err = s.doTxValidation(validateCtx, tx) + if err != nil { + return 0, nil, err + } + + consumed := validateCtx.meter.Limit() - validateCtx.meter.Remaining() + + return consumed, validateCtx.events, applyStateChanges(state, validateState) +} + +// execTx executes the tx messages on the provided state. If the tx fails then the state is discarded. +func (s STF[T]) execTx( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, + hi header.Info, +) ([]transaction.Msg, uint64, []event.Event, error) { + execState := s.branchFn(state) + + msgsResp, gasUsed, runTxMsgsEvents, txErr := s.runTxMsgs(ctx, execState, gasLimit, tx, execMode, hi) + if txErr != nil { + // in case of error during message execution, we do not apply the exec state. + // instead we run the post exec handler in a new branchFn from the initial state. + postTxState := s.branchFn(state) + postTxCtx := s.makeContext(ctx, RuntimeIdentity, postTxState, execMode) + postTxCtx.setHeaderInfo(hi) + + postTxErr := s.postTxExec(postTxCtx, tx, false) + if postTxErr != nil { + // if the post tx handler fails, then we do not apply any state change to the initial state. + // we just return the exec gas used and a joined error from TX error and post TX error. + return nil, gasUsed, nil, errors.Join(txErr, postTxErr) + } + // in case post tx is successful, then we commit the post tx state to the initial state, + // and we return post tx events alongside exec gas used and the error of the tx. + applyErr := applyStateChanges(state, postTxState) + if applyErr != nil { + return nil, 0, nil, applyErr + } + return nil, gasUsed, postTxCtx.events, txErr + } + // tx execution went fine, now we use the same state to run the post tx exec handler, + // in case the execution of the post tx fails, then no state change is applied and the + // whole execution step is rolled back. + postTxCtx := s.makeContext(ctx, RuntimeIdentity, execState, execMode) // NO gas limit. + postTxCtx.setHeaderInfo(hi) + postTxErr := s.postTxExec(postTxCtx, tx, true) + if postTxErr != nil { + // if post tx fails, then we do not apply any state change, we return the post tx error, + // alongside the gas used. + return nil, gasUsed, nil, postTxErr + } + // both the execution and post tx execution step were successful, so we apply the state changes + // to the provided state, and we return responses, and events from exec tx and post tx exec. + applyErr := applyStateChanges(state, execState) + if applyErr != nil { + return nil, 0, nil, applyErr + } + + return msgsResp, gasUsed, append(runTxMsgsEvents, postTxCtx.events...), nil +} + +// runTxMsgs will execute the messages contained in the TX with the provided state. +func (s STF[T]) runTxMsgs( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, + hi header.Info, +) ([]transaction.Msg, uint64, []event.Event, error) { + txSenders, err := tx.GetSenders() + if err != nil { + return nil, 0, nil, err + } + msgs, err := tx.GetMessages() + if err != nil { + return nil, 0, nil, err + } + msgResps := make([]transaction.Msg, len(msgs)) + + execCtx := s.makeContext(ctx, RuntimeIdentity, state, execMode) + execCtx.setHeaderInfo(hi) + execCtx.setGasLimit(gasLimit) + for i, msg := range msgs { + execCtx.sender = txSenders[i] + resp, err := s.msgRouter.Invoke(execCtx, msg) + if err != nil { + return nil, 0, nil, err // do not wrap the error or we lose the original error type + } + msgResps[i] = resp + } + + consumed := execCtx.meter.Limit() - execCtx.meter.Remaining() + return msgResps, consumed, execCtx.events, nil +} + +// preBlock executes the pre block logic. +func (s STF[T]) preBlock( + ctx *executionContext, + txs []T, +) ([]event.Event, error) { + err := s.doPreBlock(ctx, txs) + if err != nil { + return nil, err + } + + for i := range ctx.events { + ctx.events[i].BlockStage = appdata.PreBlockStage + } + + return ctx.events, nil +} + +// beginBlock executes the begin block logic. +func (s STF[T]) beginBlock( + ctx *executionContext, +) (beginBlockEvents []event.Event, err error) { + err = s.doBeginBlock(ctx) + if err != nil { + return nil, err + } + + for i := range ctx.events { + ctx.events[i].BlockStage = appdata.BeginBlockStage + } + + return ctx.events, nil +} + +// endBlock executes the end block logic. +func (s STF[T]) endBlock( + ctx *executionContext, +) ([]event.Event, []appmodulev2.ValidatorUpdate, error) { + err := s.doEndBlock(ctx) + if err != nil { + return nil, nil, err + } + + events, valsetUpdates, err := s.validatorUpdates(ctx) + if err != nil { + return nil, nil, err + } + + ctx.events = append(ctx.events, events...) + + for i := range ctx.events { + ctx.events[i].BlockStage = appdata.EndBlockStage + } + + return ctx.events, valsetUpdates, nil +} + +// validatorUpdates returns the validator updates for the current block. It is called by endBlock after the endblock execution has concluded +func (s STF[T]) validatorUpdates( + ctx *executionContext, +) ([]event.Event, []appmodulev2.ValidatorUpdate, error) { + valSetUpdates, err := s.doValidatorUpdate(ctx) + if err != nil { + return nil, nil, err + } + return ctx.events, valSetUpdates, nil +} + +// Simulate simulates the execution of a tx on the provided state. +func (s STF[T]) Simulate( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, +) (server.TxResult, store.WriterMap) { + simulationState := s.branchFn(state) + hi, err := s.getHeaderInfo(simulationState) + if err != nil { + return server.TxResult{}, nil + } + txr := s.deliverTx(ctx, simulationState, tx, internal.ExecModeSimulate, hi) + + return txr, simulationState +} + +// ValidateTx will run only the validation steps required for a transaction. +// Validations are run over the provided state, with the provided gas limit. +func (s STF[T]) ValidateTx( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, +) server.TxResult { + validationState := s.branchFn(state) + gasUsed, events, err := s.validateTx(ctx, validationState, gasLimit, tx, transaction.ExecModeCheck) + return server.TxResult{ + Events: events, + GasUsed: gasUsed, + Error: err, + } +} + +// Query executes the query on the provided state with the provided gas limits. +func (s STF[T]) Query( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + req transaction.Msg, +) (transaction.Msg, error) { + queryState := s.branchFn(state) + hi, err := s.getHeaderInfo(queryState) + if err != nil { + return nil, err + } + queryCtx := s.makeContext(ctx, nil, queryState, internal.ExecModeSimulate) + queryCtx.setHeaderInfo(hi) + queryCtx.setGasLimit(gasLimit) + return s.queryRouter.Invoke(queryCtx, req) +} + +// clone clones STF. +func (s STF[T]) clone() STF[T] { + return STF[T]{ + logger: s.logger, + msgRouter: s.msgRouter, + queryRouter: s.queryRouter, + doPreBlock: s.doPreBlock, + doBeginBlock: s.doBeginBlock, + doEndBlock: s.doEndBlock, + doValidatorUpdate: s.doValidatorUpdate, + doTxValidation: s.doTxValidation, + postTxExec: s.postTxExec, + branchFn: s.branchFn, + makeGasMeter: s.makeGasMeter, + makeGasMeteredState: s.makeGasMeteredState, + } +} + +// executionContext is a struct that holds the context for the execution of a tx. +type executionContext struct { + context.Context + + // unmeteredState is storage without metering. Changes here are propagated to state which is the metered + // version. + unmeteredState store.WriterMap + // state is the gas metered state. + state store.WriterMap + // meter is the gas meter. + meter gas.Meter + // events are the current events. + events []event.Event + // sender is the causer of the state transition. + sender transaction.Identity + // headerInfo contains the block info. + headerInfo header.Info + // execMode retains information about the exec mode. + execMode transaction.ExecMode + + branchFn branchFn + makeGasMeter makeGasMeterFn + makeGasMeteredStore makeGasMeteredStateFn + + msgRouter router.Service + queryRouter router.Service +} + +// setHeaderInfo sets the header info in the state to be used by queries in the future. +func (e *executionContext) setHeaderInfo(hi header.Info) { + e.headerInfo = hi +} + +// setGasLimit will update the gas limit of the *executionContext +func (e *executionContext) setGasLimit(limit uint64) { + meter := e.makeGasMeter(limit) + meteredState := e.makeGasMeteredStore(meter, e.unmeteredState) + + e.meter = meter + e.state = meteredState +} + +func (e *executionContext) Value(key any) any { + if key == executionContextKey { + return e + } + + return e.Context.Value(key) +} + +// TODO: too many calls to makeContext can be expensive +// makeContext creates and returns a new execution context for the STF[T] type. +// It takes in the following parameters: +// - ctx: The context.Context object for the execution. +// - sender: The transaction.Identity object representing the sender of the transaction. +// - state: The store.WriterMap object for accessing and modifying the state. +// - gasLimit: The maximum amount of gas allowed for the execution. +// - execMode: The corecontext.ExecMode object representing the execution mode. +// +// It returns a pointer to the executionContext struct +func (s STF[T]) makeContext( + ctx context.Context, + sender transaction.Identity, + store store.WriterMap, + execMode transaction.ExecMode, +) *executionContext { + valuedCtx := context.WithValue(ctx, corecontext.ExecModeKey, execMode) + return newExecutionContext( + valuedCtx, + s.makeGasMeter, + s.makeGasMeteredState, + s.branchFn, + sender, + store, + execMode, + s.msgRouter, + s.queryRouter, + ) +} + +func newExecutionContext( + ctx context.Context, + makeGasMeterFn makeGasMeterFn, + makeGasMeteredStoreFn makeGasMeteredStateFn, + branchFn branchFn, + sender transaction.Identity, + state store.WriterMap, + execMode transaction.ExecMode, + msgRouter coreRouterImpl, + queryRouter coreRouterImpl, +) *executionContext { + meter := makeGasMeterFn(gas.NoGasLimit) + meteredState := makeGasMeteredStoreFn(meter, state) + + return &executionContext{ + Context: ctx, + unmeteredState: state, + state: meteredState, + meter: meter, + events: make([]event.Event, 0), + headerInfo: header.Info{}, + execMode: execMode, + sender: sender, + branchFn: branchFn, + makeGasMeter: makeGasMeterFn, + makeGasMeteredStore: makeGasMeteredStoreFn, + msgRouter: msgRouter, + queryRouter: queryRouter, + } +} + +// applyStateChanges applies the state changes from the source store to the destination store. +// It retrieves the state changes from the source store using GetStateChanges method, +// and then applies those changes to the destination store using ApplyStateChanges method. +// If an error occurs during the retrieval or application of state changes, it is returned. +func applyStateChanges(dst, src store.WriterMap) error { + changes, err := src.GetStateChanges() + if err != nil { + return err + } + return dst.ApplyStateChanges(changes) +} + +// isCtxCancelled reports if the context was canceled. +func isCtxCancelled(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} diff --git a/tests/systemtests/testnet_init.go b/tests/systemtests/testnet_init.go index c3cd9c0f73e7..43c5409a8ced 100644 --- a/tests/systemtests/testnet_init.go +++ b/tests/systemtests/testnet_init.go @@ -144,7 +144,6 @@ func (s ModifyConfigYamlInitializer) Initialize() { EditToml(filepath.Join(nodeDir, "app.toml"), func(doc *tomledit.Document) { UpdatePort(doc, apiPortStart+i, "api", "address") UpdatePort(doc, grpcPortStart+i, "grpc", "address") - SetBool(doc, true, "grpc-web", "enable") }) } } diff --git a/tests/systemtests/unordered_tx_test.go b/tests/systemtests/unordered_tx_test.go index 6b1a9c743e7d..579552efe0e7 100644 --- a/tests/systemtests/unordered_tx_test.go +++ b/tests/systemtests/unordered_tx_test.go @@ -12,7 +12,8 @@ import ( ) func TestUnorderedTXDuplicate(t *testing.T) { - t.Skip("The unordered tx antehanlder is missing in v2") + t.Skip("The unordered tx handling is not wired in v2") + // scenario: test unordered tx duplicate // given a running chain with a tx in the unordered tx pool // when a new tx with the same hash is broadcasted diff --git a/x/auth/ante/basic_test.go b/x/auth/ante/basic_test.go index fd0dc29c1f0c..1a5687b92f62 100644 --- a/x/auth/ante/basic_test.go +++ b/x/auth/ante/basic_test.go @@ -99,7 +99,7 @@ func TestValidateMemo(t *testing.T) { } func TestConsumeGasForTxSize(t *testing.T) { - t.Skip() // TODO(@julienrbrt) Fix after https://github.com/cosmos/cosmos-sdk/pull/20072 + t.Skip() // TO FIX BEFORE 0.52 FINAL. suite := SetupTestSuite(t, true)