From 4c6e34be28c15832adb5ee91bfb235073e73b60c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:18:55 +0100 Subject: [PATCH] feat(sims): Integration with app v2 (backport #23013) (#23267) Co-authored-by: Alexander Peters Co-authored-by: Julien Robert --- .github/workflows/build.yml | 11 +- CHANGELOG.md | 4 + client/flags/flags.go | 1 + scripts/build/build.mk | 15 +- scripts/build/simulations.mk | 5 + server/v2/cometbft/go.mod | 8 +- server/v2/cometbft/go.sum | 16 +- simapp/v2/go.mod | 22 +- simapp/v2/go.sum | 36 +- simapp/v2/sim_runner.go | 492 ++++++++++++++++++++++++++++ simapp/v2/sim_test.go | 7 + simsx/msg_factory.go | 14 +- simsx/registry.go | 36 +- simsx/registry_test.go | 7 + simsx/runner.go | 17 +- simsx/v2/msg_factory.go | 31 ++ simsx/v2/registry.go | 65 ++++ simsx/v2/txutils.go | 198 +++++++++++ simsx/v2/valset.go | 108 ++++++ simsx/v2/valset_history.go | 78 +++++ testutil/sims/state_helpers.go | 3 + testutil/sims/tx_helpers.go | 11 +- testutil/testdata/tx.go | 3 +- x/gov/simulation/msg_factory.go | 3 +- x/nft/simulation/msg_factory.go | 4 +- x/staking/simulation/msg_factory.go | 24 +- 26 files changed, 1142 insertions(+), 77 deletions(-) create mode 100644 simapp/v2/sim_runner.go create mode 100644 simapp/v2/sim_test.go create mode 100644 simsx/v2/msg_factory.go create mode 100644 simsx/v2/registry.go create mode 100644 simsx/v2/txutils.go create mode 100644 simsx/v2/valset.go create mode 100644 simsx/v2/valset_history.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00b59d0186..192514be6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # go-arch: ["amd64", "arm", "arm64"] go-arch: ["amd64", "arm64"] # drop 32 bit support for now (and maybe forever) steps: - uses: actions/checkout@v4 @@ -32,14 +31,16 @@ jobs: ################### #### Build App #### ################### - - name: Build with sqlite backend - run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS=v2,sqlite make build + - name: Build + run: GOARCH=${{ matrix.go-arch }} make build - name: Build with BLS12381 if: matrix.go-arch == 'amd64' - run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS="bls12381" make build + run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS=bls12381 make build - name: Build with Secp_cgo if: matrix.go-arch == 'amd64' - run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS="secp" make build + run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS=secp make build + - name: Build v2 with sqlite backend + run: GOARCH=${{ matrix.go-arch }} COSMOS_BUILD_OPTIONS=v2,sqlite make build ################### ## Build Tooling ## ################### diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2c00974e..8c941cb11b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ Every module contains its own CHANGELOG.md. Please refer to the module you are interested in. +### Features + +* (sims) [#23013](https://github.com/cosmos/cosmos-sdk/pull/23013) Integration with app v2 + ### Improvements * (codec) [#22988](https://github.com/cosmos/cosmos-sdk/pull/22988) Improve edge case handling for recursion limits. diff --git a/client/flags/flags.go b/client/flags/flags.go index d44ed31679..2dc9db7c2d 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -83,6 +83,7 @@ const ( FlagTip = "tip" FlagAux = "aux" FlagInitHeight = "initial-height" + FlagInvCheckPeriod = "inv-check-period" // FlagOutput is the flag to set the output format. // This differs from FlagOutputDocument that is used to set the output file. FlagOutput = "output" diff --git a/scripts/build/build.mk b/scripts/build/build.mk index 9f69e78cca..868fd1f762 100644 --- a/scripts/build/build.mk +++ b/scripts/build/build.mk @@ -72,6 +72,14 @@ ifeq (bls12381,$(findstring bls12381,$(COSMOS_BUILD_OPTIONS))) build_tags += bls12381 endif +# handle sqlite +ifeq (sqlite,$(findstring sqlite,$(COSMOS_BUILD_OPTIONS))) + CGO_ENABLED=1 + ifeq (arm64,$(shell go env GOARCH)) + CC=aarch64-linux-gnu-gcc + endif +endif + # benchmark module ifeq (benchmark,$(findstring benchmark,$(COSMOS_BUILD_OPTIONS))) build_tags += benchmark @@ -110,7 +118,7 @@ ifeq (debug,$(findstring debug,$(COSMOS_BUILD_OPTIONS))) BUILD_FLAGS += -gcflags "all=-N -l" endif -#? all: Run tools build +#? all: Run tools build all: build @@ -128,7 +136,10 @@ build-linux-arm64: GOOS=linux GOARCH=arm64 LEDGER_ENABLED=false $(MAKE) build $(BUILD_TARGETS): go.sum $(BUILDDIR)/ - cd ${CURRENT_DIR}/${SIMAPP} && go $@ -mod=readonly $(BUILD_FLAGS) $(BUILD_ARGS) ./... + cd ${CURRENT_DIR}/${SIMAPP} && \ + $(if $(CGO_ENABLED),CGO_ENABLED=$(CGO_ENABLED)) \ + $(if $(CC),CC=$(CC)) \ + go $@ -mod=readonly $(BUILD_FLAGS) $(BUILD_ARGS) ./... $(BUILDDIR)/: mkdir -p $(BUILDDIR)/ diff --git a/scripts/build/simulations.mk b/scripts/build/simulations.mk index 75182afc2c..71d18b8eae 100644 --- a/scripts/build/simulations.mk +++ b/scripts/build/simulations.mk @@ -52,6 +52,11 @@ test-sim-multi-seed-short: @cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation \ -NumBlocks=50 -Period=10 -FauxMerkle=true +test-v2-sim: + @echo "Running short multi-seed application simulation. This may take awhile!" + @cd ${CURRENT_DIR}/simapp/v2 && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestSimsAppV2 +.PHONY: test-v2-sim + test-sim-benchmark-invariants: @echo "Running simulation invariant benchmarks..." diff --git a/server/v2/cometbft/go.mod b/server/v2/cometbft/go.mod index b7fe313c4e..3852f92c34 100644 --- a/server/v2/cometbft/go.mod +++ b/server/v2/cometbft/go.mod @@ -12,9 +12,9 @@ require ( cosmossdk.io/errors/v2 v2.0.0 cosmossdk.io/log v1.5.0 cosmossdk.io/schema v1.0.0 //main - cosmossdk.io/server/v2 v2.0.0-beta.1 // main - cosmossdk.io/server/v2/appmanager v1.0.0-beta.1 // main - cosmossdk.io/server/v2/stf v1.0.0-beta.1 // main + cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 // main + cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090 // main + cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090 // main cosmossdk.io/store/v2 v2.0.0-beta.1 // main cosmossdk.io/x/consensus v0.2.0-rc.1 github.com/cometbft/cometbft v1.0.0 @@ -25,7 +25,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.36.1 + google.golang.org/protobuf v1.36.2 sigs.k8s.io/yaml v1.4.0 ) diff --git a/server/v2/cometbft/go.sum b/server/v2/cometbft/go.sum index 06483795e3..b398c520d1 100644 --- a/server/v2/cometbft/go.sum +++ b/server/v2/cometbft/go.sum @@ -24,12 +24,12 @@ cosmossdk.io/math v1.5.0 h1:sbOASxee9Zxdjd6OkzogvBZ25/hP929vdcYcBJQbkLc= cosmossdk.io/math v1.5.0/go.mod h1:AAwwBmUhqtk2nlku174JwSll+/DepUXW3rWIXN5q+Nw= cosmossdk.io/schema v1.0.0 h1:/diH4XJjpV1JQwuIozwr+A4uFuuwanFdnw2kKeiXwwQ= cosmossdk.io/schema v1.0.0/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= -cosmossdk.io/server/v2 v2.0.0-beta.1 h1:mMfWf7wVEv6oVZcgxEZ52x8h/bOuGRjdSrUAEqNQmf4= -cosmossdk.io/server/v2 v2.0.0-beta.1/go.mod h1:lfkdTKM2tMlt6KhNi5f9zT+Ww5HcRpnINWjeKXCHjmc= -cosmossdk.io/server/v2/appmanager v1.0.0-beta.1 h1:EISWki+z9SDAt3OJnUl2y5Ow4qjOb+epYjLb1C7CN/E= -cosmossdk.io/server/v2/appmanager v1.0.0-beta.1/go.mod h1:RVYxIaEdIT10nWSRqbwKDLFWfvCVx+cwAorCyPAQg9A= -cosmossdk.io/server/v2/stf v1.0.0-beta.1 h1:s+nRgjhKVC08/qpr51eFVodLhyyQ9ASvJBanLBfQVNI= -cosmossdk.io/server/v2/stf v1.0.0-beta.1/go.mod h1:nfjihbofEF2GGadkYSFmgy5tqrAnSrmGcXUDZmmWyi8= +cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 h1:Votc1YWghrdTWsKipvNt6lLCGr9EXPkptlrwG83PyGE= +cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:EOOB4eUcxbfkhOLWyqA0kWHltrh6OnNp0VYhY1+8xDc= +cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090 h1:BlNFJZGtVsyaxydK3dIpklhQubbj5Sr2Eqh1Wow0lL4= +cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:l6oCGNcucF6/U949UwRj+RemNzq5475ovNHohcvN1YM= +cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090 h1:K0HN3TD7jz0zJIbvv1X5gkvEqhcBLqz71T4Al2ujols= +cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:O6Njxje0LbvC0RxdgwTmvBlbGcpKOfhbkdAguyq0ntQ= cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43 h1:glZ6MpmD+5AhwJYV4jzx+rn7cgUB2owHgk9o+93luz0= cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43/go.mod h1:XCWpgfueHSBY+B7Cf2Aq/CcsU+6XoFH+EmseCKglFrU= cosmossdk.io/store/v2 v2.0.0-beta.1 h1:p1fdZ9uNijhpXZXdqs0QS6NmXNDVPNyT4DHV4yQnF64= @@ -710,8 +710,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/simapp/v2/go.mod b/simapp/v2/go.mod index 3551abd135..09e3548215 100644 --- a/simapp/v2/go.mod +++ b/simapp/v2/go.mod @@ -11,10 +11,11 @@ require ( cosmossdk.io/indexer/postgres v0.1.0 cosmossdk.io/log v1.5.0 cosmossdk.io/math v1.5.0 - cosmossdk.io/runtime/v2 v2.0.0-20241219154748-69025c556666 // main - cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250106081242-884a7a51c3c3 // main + cosmossdk.io/runtime/v2 v2.0.0-20250109081935-cf721a654090 // main + cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 // main + cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090 cosmossdk.io/server/v2/cometbft v1.0.0-beta.1 - cosmossdk.io/store/v2 v2.0.0-beta.1 // main + cosmossdk.io/store/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 // main cosmossdk.io/tools/benchmark v0.2.0-rc.1 cosmossdk.io/tools/confix v0.2.0-rc.1 cosmossdk.io/x/accounts v0.2.0-rc.1 @@ -38,6 +39,7 @@ require ( cosmossdk.io/x/staking v0.2.0-rc.1 cosmossdk.io/x/upgrade v0.2.0-rc.1 github.com/cometbft/cometbft v1.0.0 + github.com/cometbft/cometbft/api v1.0.0 // this version is not used as it is always replaced by the latest Cosmos SDK version github.com/cosmos/cosmos-sdk v0.52.0 github.com/jackc/pgx/v5 v5.7.1 @@ -45,7 +47,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - google.golang.org/protobuf v1.36.1 + google.golang.org/protobuf v1.36.2 ) require ( @@ -61,8 +63,7 @@ require ( cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/errors/v2 v2.0.0 // indirect cosmossdk.io/schema v1.0.0 // indirect - cosmossdk.io/server/v2/appmanager v1.0.0-beta.1 // indirect; main - cosmossdk.io/server/v2/stf v1.0.0-beta.1 // indirect; main + cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090 // indirect; main cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43 // indirect; main cosmossdk.io/x/tx v1.0.0 // indirect; main filippo.io/edwards25519 v1.1.0 // indirect @@ -91,7 +92,6 @@ require ( github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/cometbft/cometbft-db v1.0.1 // indirect - github.com/cometbft/cometbft/api v1.0.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.1 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect @@ -246,11 +246,19 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) +require ( + github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e // indirect + github.com/bvinc/go-sqlite-lite v0.6.1 // indirect + github.com/cosmos/iavl/v2 v2.0.0-alpha.4 // indirect + github.com/kocubinski/costor-api v1.1.1 // indirect +) + // Here are the short-lived replace from the SimApp // Replace here are pending PRs, or version to be tagged // replace ( // // ) +replace cosmossdk.io/server/v2/cometbft => ../../server/v2/cometbft // Below are the long-lived replace of the SimApp replace ( diff --git a/simapp/v2/go.sum b/simapp/v2/go.sum index 0d6007f134..a6ebd99eab 100644 --- a/simapp/v2/go.sum +++ b/simapp/v2/go.sum @@ -214,22 +214,20 @@ cosmossdk.io/log v1.5.0 h1:dVdzPJW9kMrnAYyMf1duqacoidB9uZIl+7c6z0mnq0g= cosmossdk.io/log v1.5.0/go.mod h1:Tr46PUJjiUthlwQ+hxYtUtPn4D/oCZXAkYevBeh5+FI= cosmossdk.io/math v1.5.0 h1:sbOASxee9Zxdjd6OkzogvBZ25/hP929vdcYcBJQbkLc= cosmossdk.io/math v1.5.0/go.mod h1:AAwwBmUhqtk2nlku174JwSll+/DepUXW3rWIXN5q+Nw= -cosmossdk.io/runtime/v2 v2.0.0-20241219154748-69025c556666 h1:zFMi1URvs3NiJdSf2zPFe2mQINfBmC07F99LHQ4asyM= -cosmossdk.io/runtime/v2 v2.0.0-20241219154748-69025c556666/go.mod h1:C0MTcmQlZFPQvYoXzYiWP+h9lFOgezzafEFiQBa6xYo= +cosmossdk.io/runtime/v2 v2.0.0-20250109081935-cf721a654090 h1:zAgv9XPOMdSkwqd2Vh1AKV8MlB9iWAWCide5ShK+f2o= +cosmossdk.io/runtime/v2 v2.0.0-20250109081935-cf721a654090/go.mod h1:YRfC3bBBnZYvz0W8VubR/uImEYqi/qBH6cVAMVx+KOA= cosmossdk.io/schema v1.0.0 h1:/diH4XJjpV1JQwuIozwr+A4uFuuwanFdnw2kKeiXwwQ= cosmossdk.io/schema v1.0.0/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= -cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250106081242-884a7a51c3c3 h1:r7zBzz+Xs0rAGx/ia/5tNgM2Y4xa+SkU+Rli1mIsTRI= -cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250106081242-884a7a51c3c3/go.mod h1:vqgLhoQRRFyRUrLrDj7S18ojvgP8f0X+S1t6CME6GcQ= -cosmossdk.io/server/v2/appmanager v1.0.0-beta.1 h1:EISWki+z9SDAt3OJnUl2y5Ow4qjOb+epYjLb1C7CN/E= -cosmossdk.io/server/v2/appmanager v1.0.0-beta.1/go.mod h1:RVYxIaEdIT10nWSRqbwKDLFWfvCVx+cwAorCyPAQg9A= -cosmossdk.io/server/v2/cometbft v1.0.0-beta.1 h1:kzTMGqIwcpcynbnU0Wyoe0HObyUUBZbkD/sHBUrbZl8= -cosmossdk.io/server/v2/cometbft v1.0.0-beta.1/go.mod h1:EUvCCLYfSMEMp48KqYoHJIGMjg4Md+NcZRq4HIhjq9E= -cosmossdk.io/server/v2/stf v1.0.0-beta.1 h1:s+nRgjhKVC08/qpr51eFVodLhyyQ9ASvJBanLBfQVNI= -cosmossdk.io/server/v2/stf v1.0.0-beta.1/go.mod h1:nfjihbofEF2GGadkYSFmgy5tqrAnSrmGcXUDZmmWyi8= +cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 h1:Votc1YWghrdTWsKipvNt6lLCGr9EXPkptlrwG83PyGE= +cosmossdk.io/server/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:EOOB4eUcxbfkhOLWyqA0kWHltrh6OnNp0VYhY1+8xDc= +cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090 h1:BlNFJZGtVsyaxydK3dIpklhQubbj5Sr2Eqh1Wow0lL4= +cosmossdk.io/server/v2/appmanager v1.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:l6oCGNcucF6/U949UwRj+RemNzq5475ovNHohcvN1YM= +cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090 h1:K0HN3TD7jz0zJIbvv1X5gkvEqhcBLqz71T4Al2ujols= +cosmossdk.io/server/v2/stf v1.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:O6Njxje0LbvC0RxdgwTmvBlbGcpKOfhbkdAguyq0ntQ= cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43 h1:glZ6MpmD+5AhwJYV4jzx+rn7cgUB2owHgk9o+93luz0= cosmossdk.io/store v1.10.0-rc.1.0.20241218084712-ca559989da43/go.mod h1:XCWpgfueHSBY+B7Cf2Aq/CcsU+6XoFH+EmseCKglFrU= -cosmossdk.io/store/v2 v2.0.0-beta.1 h1:p1fdZ9uNijhpXZXdqs0QS6NmXNDVPNyT4DHV4yQnF64= -cosmossdk.io/store/v2 v2.0.0-beta.1/go.mod h1:qHQmf/9mnsXwo/Ypp2u2Zs6BmkYcx1R/Jrpyn9Ro13A= +cosmossdk.io/store/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090 h1:ZXFcStI+zFuHEBvuIPaLzPUp54PCxCAmR2RUA8/1KQM= +cosmossdk.io/store/v2 v2.0.0-beta.1.0.20250109081935-cf721a654090/go.mod h1:G+ZRihxC4jFhLXnYt4K16tSYtkvh7ayjSaYGWJFObAo= cosmossdk.io/tools/benchmark v0.2.0-rc.1 h1:Jgk0FLvnMQJrivrSMhKQBwaTiJz6MGq5ZbaHag7Sqq0= cosmossdk.io/tools/benchmark v0.2.0-rc.1/go.mod h1:tnGa8L7xHFMp26FbLvU5MRlS89BFrCxSOKR8jCi7kVU= cosmossdk.io/tools/confix v0.2.0-rc.1 h1:sVYXR89OKW19oCnr232m9/pE3+oJllNTZlTypWhXHNI= @@ -305,6 +303,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.54.6 h1:HEYUib3yTt8E6vxjMWM3yAq5b+qjj/6aKA62mkgux9g= github.com/aws/aws-sdk-go v1.54.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e h1:dSeuFcs4WAJJnswS8vXy7YY1+fdlbVPuEVmDAfqvFOQ= +github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e/go.mod h1:uh71c5Vc3VNIplXOFXsnDy21T1BepgT32c5X/YPrOyc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -324,6 +324,8 @@ github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/ github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bvinc/go-sqlite-lite v0.6.1 h1:JU8Rz5YAOZQiU3WEulKF084wfXpytRiqD2IaW2QjPz4= +github.com/bvinc/go-sqlite-lite v0.6.1/go.mod h1:2GiE60NUdb0aNhDdY+LXgrqAVDpi2Ijc6dB6ZMp9x6s= github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -402,6 +404,10 @@ github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fr github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/cosmos/iavl v1.3.5 h1:wTDFbaa/L0FVUrwTlzMnjN3fphtKgWxgcZmTc45MZuA= github.com/cosmos/iavl v1.3.5/go.mod h1:T6SfBcyhulVIY2G/ZtAtQm/QiJvsuhIos52V4dWYk88= +github.com/cosmos/iavl-bench/bench v0.0.4 h1:J6zQPiBqF4CXMM3QBsLqZgQEBGY0taX85vLIZMhmAfQ= +github.com/cosmos/iavl-bench/bench v0.0.4/go.mod h1:j2rLae77EffacWcp7mmj3Uaa4AOAmZA7ymvhsuBQKKI= +github.com/cosmos/iavl/v2 v2.0.0-alpha.4 h1:PfpQt7xl4hojw2UFS2JdJppJnx8sjlmcxRQ7Hxk7Cl0= +github.com/cosmos/iavl/v2 v2.0.0-alpha.4/go.mod h1:7RSm0aeApe3S1x4TrLffvUL6pjOtMYV4glYnpAhr2lw= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= @@ -702,6 +708,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kocubinski/costor-api v1.1.1 h1:sgfJA7T/8IfZ59zxiMrED0xdjerAFuPNBTqyO90GiEE= +github.com/kocubinski/costor-api v1.1.1/go.mod h1:ESMBMDkKfN+9vvvhhNVdKLhbOmzI3O/i16iXvRM9Tuc= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1484,8 +1492,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/simapp/v2/sim_runner.go b/simapp/v2/sim_runner.go new file mode 100644 index 0000000000..3ae56ad778 --- /dev/null +++ b/simapp/v2/sim_runner.go @@ -0,0 +1,492 @@ +package simapp + +import ( + "context" + "encoding/json" + "fmt" + "iter" + "maps" + "math/rand" + "slices" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2" + "cosmossdk.io/server/v2/appmanager" + cometbfttypes "cosmossdk.io/server/v2/cometbft/types" + storev2 "cosmossdk.io/store/v2" + consensustypes "cosmossdk.io/x/consensus/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simsx" + simsxv2 "github.com/cosmos/cosmos-sdk/simsx/v2" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +type Tx = transaction.Tx +type ( + HasWeightedOperationsX = simsx.HasWeightedOperationsX + HasWeightedOperationsXWithProposals = simsx.HasWeightedOperationsXWithProposals + HasProposalMsgsX = simsx.HasProposalMsgsX +) + +const SimAppChainID = "simulation-app" + +// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32 +var defaultSeeds = []int64{ + 1, 2, 4, 7, + 32, 123, 124, 582, 1893, 2989, + 3012, 4728, 37827, 981928, 87821, 891823782, + 989182, 89182391, 11, 22, 44, 77, 99, 2020, + 3232, 123123, 124124, 582582, 18931893, + 29892989, 30123012, 47284728, 7601778, 8090485, + 977367484, 491163361, 424254581, 673398983, +} + +const ( + maxTimePerBlock = 10_000 * time.Second + minTimePerBlock = maxTimePerBlock / 2 + timeRangePerBlock = maxTimePerBlock - minTimePerBlock +) + +type ( + AuthKeeper interface { + simsx.ModuleAccountSource + simsx.AccountSource + } + + BankKeeper interface { + simsx.BalanceSource + GetBlockedAddresses() map[string]bool + } + + StakingKeeper interface { + UnbondingTime(ctx context.Context) (time.Duration, error) + } + + ModuleManager interface { + Modules() map[string]appmodulev2.AppModule + } + + // SimulationApp abstract blockchain app + SimulationApp[T Tx] interface { + GetApp() *runtime.App[T] + TxConfig() client.TxConfig + AppCodec() codec.Codec + DefaultGenesis() map[string]json.RawMessage + Store() storev2.RootStore + Close() error + } + + // TestInstance system under test + TestInstance[T Tx] struct { + App SimulationApp[T] + TxDecoder transaction.Codec[T] + BankKeeper BankKeeper + AuthKeeper AuthKeeper + StakingKeeper StakingKeeper + TXBuilder simsxv2.TXBuilder[T] + AppManager appmanager.AppManager[T] + ModuleManager ModuleManager + } + + AppFactory[T Tx, V SimulationApp[T]] func(config depinject.Config, outputs ...any) (V, error) +) + +// SetupTestInstance initializes and returns the system under test. +func SetupTestInstance[T Tx, V SimulationApp[T]](t *testing.T, factory AppFactory[T, V], appConfig depinject.Config) TestInstance[T] { + t.Helper() + vp := viper.New() + vp.Set("store.app-db-backend", "memdb") + vp.Set("home", t.TempDir()) + + depInjCfg := depinject.Configs( + depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings())), + appConfig, + ) + var ( + bankKeeper BankKeeper + authKeeper AuthKeeper + stKeeper StakingKeeper + ) + + err := depinject.Inject(depInjCfg, + &authKeeper, + &bankKeeper, + &stKeeper, + ) + require.NoError(t, err) + + xapp, err := factory(depinject.Configs(depinject.Supply(log.NewNopLogger(), runtime.GlobalConfig(vp.AllSettings())))) + require.NoError(t, err) + return TestInstance[T]{ + App: xapp, + BankKeeper: bankKeeper, + AuthKeeper: authKeeper, + StakingKeeper: stKeeper, + AppManager: xapp.GetApp(), + ModuleManager: xapp.GetApp().ModuleManager(), + TxDecoder: simsxv2.NewGenericTxDecoder[T](xapp.TxConfig()), + TXBuilder: simsxv2.NewSDKTXBuilder[T](xapp.TxConfig(), simsxv2.DefaultGenTxGas), + } +} + +// RunWithSeeds runs a series of subtests using the default set of random seeds for deterministic simulation testing. +func RunWithSeeds[T Tx]( + t *testing.T, + seeds []int64, + postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account), +) { + t.Helper() + cfg := cli.NewConfigFromFlags() + cfg.ChainID = SimAppChainID + for i := range seeds { + seed := seeds[i] + t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) { + t.Parallel() + RunWithSeed(t, NewSimApp[T], AppConfig(), cfg, seed, postRunActions...) + }) + } +} + +// RunWithSeed initializes and executes a simulation run with the given seed, generating blocks and transactions. +func RunWithSeed[T Tx, V SimulationApp[T]]( + t *testing.T, + appFactory AppFactory[T, V], + appConfig depinject.Config, + tCfg simtypes.Config, + seed int64, + postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account), +) { + t.Helper() + r := rand.New(rand.NewSource(seed)) + testInstance := SetupTestInstance[T, V](t, appFactory, appConfig) + accounts, genesisAppState, chainID, genesisTimestamp := prepareInitialGenesisState(testInstance.App, r, testInstance.BankKeeper, tCfg, testInstance.ModuleManager) + + appManager := testInstance.AppManager + appStore := testInstance.App.Store() + txConfig := testInstance.App.TxConfig() + rootCtx, done := context.WithCancel(context.Background()) + defer done() + initRsp, stateRoot := doChainInitWithGenesis(t, rootCtx, chainID, genesisTimestamp, appManager, testInstance.TxDecoder, genesisAppState, appStore) + activeValidatorSet := simsxv2.NewValSet().Update(initRsp.ValidatorUpdates) + valsetHistory := simsxv2.NewValSetHistory(1) + valsetHistory.Add(genesisTimestamp, activeValidatorSet) + + emptySimParams := make(map[string]json.RawMessage) // todo read sims params from disk as before + + modules := testInstance.ModuleManager.Modules() + msgFactoriesFn := prepareSimsMsgFactories(r, modules, simsx.ParamWeightSource(emptySimParams)) + + cs := chainState[T]{ + chainID: chainID, + blockTime: genesisTimestamp, + activeValidatorSet: activeValidatorSet, + valsetHistory: valsetHistory, + stateRoot: stateRoot, + app: appManager, + appStore: appStore, + txConfig: txConfig, + } + doMainLoop( + t, + rootCtx, + cs, + msgFactoriesFn, + r, + testInstance.AuthKeeper, + testInstance.BankKeeper, + accounts, + testInstance.TXBuilder, + testInstance.StakingKeeper, + ) + require.NoError(t, testInstance.App.Close(), "closing app") + + for _, step := range postRunActions { + step(t, testInstance, accounts) + } +} + +// prepareInitialGenesisState initializes the genesis state for simulation by generating accounts, app state, chain ID, and timestamp. +// It uses a random seed, configuration parameters, and module manager to customize the state. +// Blocked accounts are removed from the simulation accounts list based on the bank keeper's configuration. +func prepareInitialGenesisState[T Tx]( + app SimulationApp[T], + r *rand.Rand, + bankKeeper BankKeeper, + tCfg simtypes.Config, + moduleManager ModuleManager, +) ([]simtypes.Account, json.RawMessage, string, time.Time) { + txConfig := app.TxConfig() + // todo: replace legacy testdata functions ? + appStateFn := simtestutil.AppStateFn( + app.AppCodec(), + txConfig.SigningContext().AddressCodec(), + txConfig.SigningContext().ValidatorAddressCodec(), + toLegacySimsModule(moduleManager.Modules()), + app.DefaultGenesis(), + ) + params := simulation.RandomParams(r) + accounts := slices.DeleteFunc(simtypes.RandomAccounts(r, params.NumKeys()), + func(acc simtypes.Account) bool { // remove blocked accounts + return bankKeeper.GetBlockedAddresses()[acc.AddressBech32] + }) + + appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, tCfg) + return accounts, appState, chainID, genesisTimestamp +} + +// doChainInitWithGenesis initializes the blockchain state with the provided genesis data and returns the initial block response and state root. +func doChainInitWithGenesis[T Tx]( + t *testing.T, + ctx context.Context, + chainID string, + genesisTimestamp time.Time, + app appmanager.AppManager[T], + txDecoder transaction.Codec[T], + genesisAppState json.RawMessage, + appStore cometbfttypes.Store, +) (*server.BlockResponse, store.Hash) { + t.Helper() + genesisReq := &server.BlockRequest[T]{ + Height: 0, + Time: genesisTimestamp, + Hash: make([]byte, 32), + ChainId: chainID, + AppHash: make([]byte, 32), + IsGenesis: true, + } + + initialConsensusParams := &consensustypes.MsgUpdateParams{ + Block: &cmtproto.BlockParams{ + MaxBytes: 200000, + MaxGas: 100_000_000, + }, + Evidence: &cmtproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &cmtproto.ValidatorParams{PubKeyTypes: []string{cmttypes.ABCIPubKeyTypeEd25519, cmttypes.ABCIPubKeyTypeSecp256k1}}, + } + genesisCtx := context.WithValue(ctx, corecontext.CometParamsInitInfoKey, initialConsensusParams) + initRsp, genesisStateChanges, err := app.InitGenesis(genesisCtx, genesisReq, genesisAppState, txDecoder) + require.NoError(t, err) + + require.NoError(t, appStore.SetInitialVersion(0)) + changeSet, err := genesisStateChanges.GetStateChanges() + require.NoError(t, err) + + stateRoot, err := appStore.Commit(&store.Changeset{Changes: changeSet}) + require.NoError(t, err) + return initRsp, stateRoot +} + +// chainState represents the state of a blockchain during a simulation run. +type chainState[T Tx] struct { + chainID string + blockTime time.Time + activeValidatorSet simsxv2.WeightedValidators + valsetHistory *simsxv2.ValSetHistory + stateRoot store.Hash + app appmanager.TransactionFuzzer[T] + appStore storev2.RootStore + txConfig client.TxConfig +} + +// doMainLoop executes the main simulation loop after chain setup with genesis block. +// Based on the initial seed and configurations, a deterministic set of messages is generated +// and executed. Events like validators missing votes or double signing are included in this +// process. The runtime tracks the validator's state and history. +func doMainLoop[T Tx]( + t *testing.T, + rootCtx context.Context, + cs chainState[T], + nextMsgFactory func() simsx.SimMsgFactoryX, + r *rand.Rand, + authKeeper AuthKeeper, + bankKeeper simsx.BalanceSource, + accounts []simtypes.Account, + txBuilder simsxv2.TXBuilder[T], + stakingKeeper StakingKeeper, +) { + t.Helper() + blockTime := cs.blockTime + activeValidatorSet := cs.activeValidatorSet + if len(activeValidatorSet) == 0 { + t.Fatal("no active validators in chain setup") + return + } + valsetHistory := cs.valsetHistory + stateRoot := cs.stateRoot + chainID := cs.chainID + app := cs.app + appStore := cs.appStore + + const ( // todo: read from CLI instead + numBlocks = 100 // 500 default + maxTXPerBlock = 200 // 200 default + ) + + var ( + txSkippedCounter int + txTotalCounter int + ) + rootReporter := simsx.NewBasicSimulationReporter() + futureOpsReg := simsxv2.NewFutureOpsRegistry() + + for i := 0; i < numBlocks; i++ { + if len(activeValidatorSet) == 0 { + t.Skipf("run out of validators in block: %d\n", i+1) + return + } + blockTime = blockTime.Add(minTimePerBlock) + blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeRangePerBlock/time.Second)))) * time.Second) + valsetHistory.Add(blockTime, activeValidatorSet) + blockReqN := &server.BlockRequest[T]{ + Height: uint64(1 + i), + Time: blockTime, + Hash: stateRoot, + AppHash: stateRoot, + ChainId: chainID, + } + + cometInfo := comet.Info{ + ValidatorsHash: nil, + Evidence: valsetHistory.MissBehaviour(r), + ProposerAddress: activeValidatorSet[0].Address, + LastCommit: activeValidatorSet.NewCommitInfo(r), + } + fOps, pos := futureOpsReg.PopScheduledFor(blockTime), 0 + addressCodec := cs.txConfig.SigningContext().AddressCodec() + simsCtx := context.WithValue(rootCtx, corecontext.CometInfoKey, cometInfo) // required for ContextAwareCometInfoService + resultHandlers := make([]simsx.SimDeliveryResultHandler, 0, maxTXPerBlock) + var txPerBlockCounter int + blockRsp, updates, err := app.DeliverSims(simsCtx, blockReqN, func(ctx context.Context) iter.Seq[T] { + return func(yield func(T) bool) { + unbondingTime, err := stakingKeeper.UnbondingTime(ctx) + require.NoError(t, err) + valsetHistory.SetMaxHistory(minBlocksInUnbondingPeriod(unbondingTime)) + testData := simsx.NewChainDataSource(ctx, r, authKeeper, bankKeeper, addressCodec, accounts...) + + for txPerBlockCounter < maxTXPerBlock { + txPerBlockCounter++ + mergedMsgFactory := func() simsx.SimMsgFactoryX { + if pos < len(fOps) { + pos++ + return fOps[pos-1] + } + return nextMsgFactory() + }() + reporter := rootReporter.WithScope(mergedMsgFactory.MsgType()) + if fx, ok := mergedMsgFactory.(simsx.HasFutureOpsRegistry); ok { + fx.SetFutureOpsRegistry(futureOpsReg) + continue + } + + // the stf context is required to access state via keepers + signers, msg := mergedMsgFactory.Create()(ctx, testData, reporter) + if reporter.IsSkipped() { + txSkippedCounter++ + require.NoError(t, reporter.Close()) + continue + } + resultHandlers = append(resultHandlers, mergedMsgFactory.DeliveryResultHandler()) + reporter.Success(msg) + require.NoError(t, reporter.Close()) + + tx, err := txBuilder.Build(ctx, authKeeper, signers, msg, r, chainID) + require.NoError(t, err) + if !yield(tx) { + return + } + } + } + }) + require.NoError(t, err, "%d, %s", blockReqN.Height, blockReqN.Time) + changeSet, err := updates.GetStateChanges() + require.NoError(t, err) + stateRoot, err = appStore.Commit(&store.Changeset{ + Version: blockReqN.Height, + Changes: changeSet, + }) + + require.NoError(t, err) + require.Equal(t, len(resultHandlers), len(blockRsp.TxResults), "txPerBlockCounter: %d, totalSkipped: %d", txPerBlockCounter, txSkippedCounter) + for i, v := range blockRsp.TxResults { + require.NoError(t, resultHandlers[i](v.Error)) + } + txTotalCounter += txPerBlockCounter + activeValidatorSet = activeValidatorSet.Update(blockRsp.ValidatorUpdates) + } + fmt.Println("+++ reporter:\n" + rootReporter.Summary().String()) + fmt.Printf("Tx total: %d skipped: %d\n", txTotalCounter, txSkippedCounter) +} + +// prepareSimsMsgFactories constructs and returns a function to retrieve simulation message factories for all modules. +// It initializes proposal and factory registries, registers proposals and weighted operations, and sorts deterministically. +func prepareSimsMsgFactories( + r *rand.Rand, + modules map[string]appmodulev2.AppModule, + weights simsx.WeightSource, +) func() simsx.SimMsgFactoryX { + moduleNames := slices.Collect(maps.Keys(modules)) + slices.Sort(moduleNames) // make deterministic + + // get all proposal types + proposalRegistry := simsx.NewUniqueTypeRegistry() + for _, n := range moduleNames { + switch xm := modules[n].(type) { // nolint: gocritic // extended in the future + case HasProposalMsgsX: + xm.ProposalMsgsX(weights, proposalRegistry) + // todo: register legacy and v1 msg proposals + } + } + // register all msg factories + factoryRegistry := simsx.NewUnorderedRegistry() + for _, n := range moduleNames { + switch xm := modules[n].(type) { + case HasWeightedOperationsX: + xm.WeightedOperationsX(weights, factoryRegistry) + case HasWeightedOperationsXWithProposals: + xm.WeightedOperationsX(weights, factoryRegistry, proposalRegistry.Iterator(), nil) + } + } + return simsxv2.NextFactoryFn(factoryRegistry.Elements(), r) +} + +func toLegacySimsModule(modules map[string]appmodule.AppModule) []module.AppModuleSimulation { + r := make([]module.AppModuleSimulation, 0, len(modules)) + names := slices.Collect(maps.Keys(modules)) + slices.Sort(names) // make deterministic + for _, v := range names { + if m, ok := modules[v].(module.AppModuleSimulation); ok { + r = append(r, m) + } + } + return r +} + +func minBlocksInUnbondingPeriod(unbondingTime time.Duration) int { + maxblocks := unbondingTime / maxTimePerBlock + return max(int(maxblocks)-1, 1) +} diff --git a/simapp/v2/sim_test.go b/simapp/v2/sim_test.go new file mode 100644 index 0000000000..c92aeaedef --- /dev/null +++ b/simapp/v2/sim_test.go @@ -0,0 +1,7 @@ +package simapp + +import "testing" + +func TestSimsAppV2(t *testing.T) { + RunWithSeeds[Tx](t, defaultSeeds) +} diff --git a/simsx/msg_factory.go b/simsx/msg_factory.go index 1c6b6e4b96..c2e751c84f 100644 --- a/simsx/msg_factory.go +++ b/simsx/msg_factory.go @@ -61,24 +61,20 @@ type ResultHandlingSimMsgFactory[T sdk.Msg] struct { // NewSimMsgFactoryWithDeliveryResultHandler constructor func NewSimMsgFactoryWithDeliveryResultHandler[T sdk.Msg](f FactoryMethodWithDeliveryResultHandler[T]) *ResultHandlingSimMsgFactory[T] { - // the result handler is always called after the factory. so we initialize it lazy for syntactic sugar and simplicity - // in the message factory function that is implemented by the users - var lazyResultHandler SimDeliveryResultHandler r := &ResultHandlingSimMsgFactory[T]{ - resultHandler: func(err error) error { - return lazyResultHandler(err) - }, + resultHandler: expectNoError, } r.SimMsgFactoryFn = func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg T) { - signer, msg, lazyResultHandler = f(ctx, testData, reporter) - if lazyResultHandler == nil { - lazyResultHandler = expectNoError + signer, msg, r.resultHandler = f(ctx, testData, reporter) + if r.resultHandler == nil { + r.resultHandler = expectNoError } return } return r } +// DeliveryResultHandler result handler of the last msg factory invocation func (f ResultHandlingSimMsgFactory[T]) DeliveryResultHandler() SimDeliveryResultHandler { return f.resultHandler } diff --git a/simsx/registry.go b/simsx/registry.go index 6aee009f37..aeb3577ee0 100644 --- a/simsx/registry.go +++ b/simsx/registry.go @@ -160,6 +160,12 @@ func NewUniqueTypeRegistry() UniqueTypeRegistry { } func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) { + if weight == 0 { + return + } + if f == nil { + panic("message factory must not be nil") + } msgType := f.MsgType() msgTypeURL := sdk.MsgTypeURL(msgType) if _, exists := s[msgTypeURL]; exists { @@ -170,8 +176,7 @@ func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) { // Iterator returns an iterator function for a Go for loop sorted by weight desc. func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter { - x := maps.Values(s) - sortedWeightedFactory := slices.SortedFunc(x, func(a, b WeightedFactory) int { + sortedWeightedFactory := slices.SortedFunc(maps.Values(s), func(a, b WeightedFactory) int { return a.Compare(b) }) @@ -184,6 +189,33 @@ func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter { } } +var _ Registry = &UnorderedRegistry{} + +// UnorderedRegistry represents a collection of WeightedFactory elements without guaranteed order. +// It is used to maintain factories coupled with their associated weights for simulation purposes. +type UnorderedRegistry []WeightedFactory + +func NewUnorderedRegistry() *UnorderedRegistry { + r := make(UnorderedRegistry, 0) + return &r +} + +// Add appends a new WeightedFactory with the provided weight and factory to the UnorderedRegistry. +func (x *UnorderedRegistry) Add(weight uint32, f SimMsgFactoryX) { + if weight == 0 { + return + } + if f == nil { + panic("message factory must not be nil") + } + *x = append(*x, WeightedFactory{Weight: weight, Factory: f}) +} + +// Elements returns all collected elements +func (x *UnorderedRegistry) Elements() []WeightedFactory { + return *x +} + // WeightedFactory is a Weight Factory tuple type WeightedFactory struct { Weight uint32 diff --git a/simsx/registry_test.go b/simsx/registry_test.go index e8aba1239c..b9ceb3d967 100644 --- a/simsx/registry_test.go +++ b/simsx/registry_test.go @@ -119,6 +119,9 @@ func TestUniqueTypeRegistry(t *testing.T) { exampleFactory := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) { return []SimAccount{}, nil }) + exampleFactory2 := SimMsgFactoryFn[*testdata.MsgCreateDog](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.MsgCreateDog) { + return []SimAccount{}, nil + }) specs := map[string]struct { src []WeightedFactory @@ -129,6 +132,10 @@ func TestUniqueTypeRegistry(t *testing.T) { src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}}, exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}}, }, + "sorted": { + src: []WeightedFactory{{Weight: 2, Factory: exampleFactory2}, {Weight: 1, Factory: exampleFactory}}, + exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}, {Weight: 2, Factory: exampleFactory2.Create()}}, + }, "duplicate": { src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}, {Weight: 2, Factory: exampleFactory}}, expErr: true, diff --git a/simsx/runner.go b/simsx/runner.go index 83d6a0b292..86992617ea 100644 --- a/simsx/runner.go +++ b/simsx/runner.go @@ -11,6 +11,7 @@ import ( dbm "github.com/cosmos/cosmos-db" "github.com/stretchr/testify/require" + "cosmossdk.io/core/server" corestore "cosmossdk.io/core/store" "cosmossdk.io/log" @@ -19,8 +20,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/runtime" - "github.com/cosmos/cosmos-sdk/server" - servertypes "github.com/cosmos/cosmos-sdk/server/types" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" @@ -77,7 +76,7 @@ func Run[T SimulationApp]( db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, - appOpts servertypes.AppOptions, + appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp), ) T, setupStateFactory func(app T) SimStateFactory, @@ -103,7 +102,7 @@ func RunWithSeeds[T SimulationApp]( db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, - appOpts servertypes.AppOptions, + appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp), ) T, setupStateFactory func(app T) SimStateFactory, @@ -123,7 +122,7 @@ func RunWithSeedsAndRandAcc[T SimulationApp]( db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, - appOpts servertypes.AppOptions, + appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp), ) T, setupStateFactory func(app T) SimStateFactory, @@ -153,7 +152,7 @@ func RunWithSeedsAndRandAcc[T SimulationApp]( func RunWithSeed[T SimulationApp]( tb testing.TB, cfg simtypes.Config, - appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T, + appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp)) T, setupStateFactory func(app T) SimStateFactory, seed int64, fuzzSeed []byte, @@ -167,7 +166,7 @@ func RunWithSeed[T SimulationApp]( func RunWithSeedAndRandAcc[T SimulationApp]( tb testing.TB, cfg simtypes.Config, - appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T, + appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp)) T, setupStateFactory func(app T) SimStateFactory, seed int64, fuzzSeed []byte, @@ -330,7 +329,7 @@ func prepareWeightedOps( func NewSimulationAppInstance[T SimulationApp]( tb testing.TB, tCfg simtypes.Config, - appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T, + appFactory func(logger log.Logger, db corestore.KVStoreWithBatch, traceStore io.Writer, loadLatest bool, appOpts server.DynamicConfig, baseAppOptions ...func(*baseapp.BaseApp)) T, ) TestInstance[T] { tb.Helper() workDir := tb.TempDir() @@ -350,7 +349,7 @@ func NewSimulationAppInstance[T SimulationApp]( }) appOptions := make(simtestutil.AppOptionsMap) appOptions[flags.FlagHome] = workDir - appOptions[server.FlagInvCheckPeriod] = cli.FlagPeriodValue + appOptions[flags.FlagInvCheckPeriod] = cli.FlagPeriodValue opts := []func(*baseapp.BaseApp){baseapp.SetChainID(tCfg.ChainID)} if tCfg.FauxMerkle { opts = append(opts, FauxMerkleModeOpt) diff --git a/simsx/v2/msg_factory.go b/simsx/v2/msg_factory.go new file mode 100644 index 0000000000..3ac1c798a5 --- /dev/null +++ b/simsx/v2/msg_factory.go @@ -0,0 +1,31 @@ +package v2 + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// NextFactoryFn shuffles and processes a list of weighted factories, returning a selection function for factory objects. +func NextFactoryFn(factories []simsx.WeightedFactory, r *rand.Rand) func() simsx.SimMsgFactoryX { + factCount := len(factories) + r.Shuffle(factCount, func(i, j int) { + factories[i], factories[j] = factories[j], factories[i] + }) + var totalWeight int + for _, f := range factories { + totalWeight += int(f.Weight) + } + return func() simsx.SimMsgFactoryX { + // this is copied from old sims WeightedOperations.getSelectOpFn + x := r.Intn(totalWeight) + for i := 0; i < factCount; i++ { + if x <= int(factories[i].Weight) { + return factories[i].Factory + } + x -= int(factories[i].Weight) + } + // shouldn't happen + return factories[0].Factory + } +} diff --git a/simsx/v2/registry.go b/simsx/v2/registry.go new file mode 100644 index 0000000000..8811fa3702 --- /dev/null +++ b/simsx/v2/registry.go @@ -0,0 +1,65 @@ +package v2 + +import ( + "math/rand" + "time" + + "github.com/huandu/skiplist" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// FutureOpsRegistry is a registry for scheduling and retrieving operations mapped to future block times. +type FutureOpsRegistry struct { + list *skiplist.SkipList +} + +// NewFutureOpsRegistry constructor +func NewFutureOpsRegistry() *FutureOpsRegistry { + list := skiplist.New(timeComparator{}) + list.SetRandSource(rand.NewSource(1)) + return &FutureOpsRegistry{list: list} +} + +// Add schedules a new operation for the given block time +func (l *FutureOpsRegistry) Add(blockTime time.Time, fx simsx.SimMsgFactoryX) { + if fx == nil { + panic("message factory must not be nil") + } + if blockTime.IsZero() { + return + } + var scheduledOps []simsx.SimMsgFactoryX + if e := l.list.Get(blockTime); e != nil { + scheduledOps = e.Value.([]simsx.SimMsgFactoryX) + } + scheduledOps = append(scheduledOps, fx) + l.list.Set(blockTime, scheduledOps) +} + +// PopScheduledFor retrieves and removes all scheduled operations up to the specified block time from the registry. +func (l *FutureOpsRegistry) PopScheduledFor(blockTime time.Time) []simsx.SimMsgFactoryX { + var r []simsx.SimMsgFactoryX + for { + e := l.list.Front() + if e == nil || e.Key().(time.Time).After(blockTime) { + break + } + r = append(r, e.Value.([]simsx.SimMsgFactoryX)...) + l.list.RemoveFront() + } + return r +} + +var _ skiplist.Comparable = timeComparator{} + +// used for SkipList +type timeComparator struct{} + +func (t timeComparator) Compare(lhs, rhs interface{}) int { + return lhs.(time.Time).Compare(rhs.(time.Time)) +} + +func (t timeComparator) CalcScore(key interface{}) float64 { + return float64(key.(time.Time).UnixNano()) +} diff --git a/simsx/v2/txutils.go b/simsx/v2/txutils.go new file mode 100644 index 0000000000..640d09a114 --- /dev/null +++ b/simsx/v2/txutils.go @@ -0,0 +1,198 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "math/rand" + + "cosmossdk.io/core/transaction" + + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/simsx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsign "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +const DefaultGenTxGas = 10_000_000 + +type Tx = transaction.Tx + +// TXBuilder abstract transaction builder +type TXBuilder[T Tx] interface { + // Build creates a signed transaction + Build(ctx context.Context, + ak simsx.AccountSource, + senders []simsx.SimAccount, + msg sdk.Msg, + r *rand.Rand, + chainID string, + ) (T, error) +} + +var _ TXBuilder[Tx] = TXBuilderFn[Tx](nil) + +// TXBuilderFn adapter that implements the TXBuilder interface +type TXBuilderFn[T Tx] func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error) + +func (b TXBuilderFn[T]) Build(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (T, error) { + return b(ctx, ak, senders, msg, r, chainID) +} + +// NewSDKTXBuilder constructor to create a signed transaction builder for sdk.Tx type. +func NewSDKTXBuilder[T Tx](txConfig client.TxConfig, defaultGas uint64) TXBuilder[T] { + return TXBuilderFn[T](func(ctx context.Context, ak simsx.AccountSource, senders []simsx.SimAccount, msg sdk.Msg, r *rand.Rand, chainID string) (tx T, err error) { + accountNumbers := make([]uint64, len(senders)) + sequenceNumbers := make([]uint64, len(senders)) + for i := 0; i < len(senders); i++ { + acc := ak.GetAccount(ctx, senders[i].Address) + accountNumbers[i] = acc.GetAccountNumber() + sequenceNumbers[i] = acc.GetSequence() + } + fees := senders[0].LiquidBalance().RandFees() + sdkTx, err := GenSignedMockTx( + r, + txConfig, + []sdk.Msg{msg}, + fees, + defaultGas, + chainID, + accountNumbers, + sequenceNumbers, + simsx.Collect(senders, func(a simsx.SimAccount) cryptotypes.PrivKey { return a.PrivKey })..., + ) + if err != nil { + return tx, err + } + out, ok := sdkTx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + return out, nil + }) +} + +// GenSignedMockTx generates a signed mock transaction. +func GenSignedMockTx( + r *rand.Rand, + txConfig client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, accSeqs []uint64, + priv ...cryptotypes.PrivKey, +) (sdk.Tx, error) { + sigs := make([]signing.SignatureV2, len(priv)) + + // create a random length memo + memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100)) + + signMode, err := authsign.APISignModeToInternal(txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return nil, err + } + + // 1st round: set SignatureV2 with empty signatures, to set correct + // signer infos. + for i, p := range priv { + sigs[i] = signing.SignatureV2{ + PubKey: p.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signMode, + }, + Sequence: accSeqs[i], + } + } + + tx := txConfig.NewTxBuilder() + err = tx.SetMsgs(msgs...) + if err != nil { + return nil, err + } + err = tx.SetSignatures(sigs...) + if err != nil { + return nil, err + } + tx.SetMemo(memo) + tx.SetFeeAmount(feeAmt) + tx.SetGasLimit(gas) + + // 2nd round: once all signer infos are set, every signer can sign. + for i, p := range priv { + signerData := authsign.SignerData{ + Address: sdk.AccAddress(p.PubKey().Address()).String(), + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + PubKey: p.PubKey(), + } + + signBytes, err := authsign.GetSignBytesAdapter( + context.Background(), txConfig.SignModeHandler(), signMode, signerData, + tx.GetTx()) + if err != nil { + return nil, fmt.Errorf("sign bytes: %w", err) + } + sig, err := p.Sign(signBytes) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + sigs[i].Data.(*signing.SingleSignatureData).Signature = sig + } + + if err = tx.SetSignatures(sigs...); err != nil { + return nil, fmt.Errorf("signature: %w", err) + } + + return tx.GetTx(), nil +} + +var _ transaction.Codec[Tx] = &GenericTxDecoder[Tx]{} + +// GenericTxDecoder Encoder type that implements transaction.Codec +type GenericTxDecoder[T Tx] struct { + txConfig client.TxConfig +} + +// NewGenericTxDecoder constructor +func NewGenericTxDecoder[T Tx](txConfig client.TxConfig) *GenericTxDecoder[T] { + return &GenericTxDecoder[T]{txConfig: txConfig} +} + +// Decode implements transaction.Codec. +func (t *GenericTxDecoder[T]) Decode(bz []byte) (T, error) { + var out T + tx, err := t.txConfig.TxDecoder()(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} + +// DecodeJSON implements transaction.Codec. +func (t *GenericTxDecoder[T]) DecodeJSON(bz []byte) (T, error) { + var out T + tx, err := t.txConfig.TxJSONDecoder()(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(T) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} diff --git a/simsx/v2/valset.go b/simsx/v2/valset.go new file mode 100644 index 0000000000..dfc57523ce --- /dev/null +++ b/simsx/v2/valset.go @@ -0,0 +1,108 @@ +package v2 + +import ( + "bytes" + "crypto/sha256" + "math/rand" + "slices" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +// WeightedValidator represents a validator for usage in the sims runner. +type WeightedValidator struct { + Power int64 + Address []byte + Offline bool +} + +// Compare determines the order between two WeightedValidator instances. +// Returns -1 if the caller has higher Power, 1 if it has lower Power, and defaults to comparing their Address bytes. +func (a WeightedValidator) Compare(b WeightedValidator) int { + switch { + case a.Power < b.Power: + return 1 + case a.Power > b.Power: + return -1 + default: + return bytes.Compare(a.Address, b.Address) + } +} + +// NewValSet constructor +func NewValSet() WeightedValidators { + return make(WeightedValidators, 0) +} + +// WeightedValidators represents a slice of WeightedValidator, used for managing and processing validator sets. +type WeightedValidators []WeightedValidator + +func (v WeightedValidators) Update(updates []appmodulev2.ValidatorUpdate) WeightedValidators { + if len(updates) == 0 { + return v + } + const truncatedSize = 20 + valUpdates := simsx.Collect(updates, func(u appmodulev2.ValidatorUpdate) WeightedValidator { + hash := sha256.Sum256(u.PubKey) + return WeightedValidator{Power: u.Power, Address: hash[:truncatedSize]} + }) + newValset := slices.Clone(v) + for _, u := range valUpdates { + pos := slices.IndexFunc(newValset, func(val WeightedValidator) bool { + return bytes.Equal(u.Address, val.Address) + }) + if pos == -1 { // new address + if u.Power > 0 { + newValset = append(newValset, u) + } + continue + } + if u.Power == 0 { + newValset = append(newValset[0:pos], newValset[pos+1:]...) + continue + } + newValset[pos].Power = u.Power + } + + newValset = slices.DeleteFunc(newValset, func(validator WeightedValidator) bool { + return validator.Power == 0 + }) + + // sort vals by Power + slices.SortFunc(newValset, func(a, b WeightedValidator) int { + return a.Compare(b) + }) + return newValset +} + +// NewCommitInfo build Comet commit info for the validator set +func (v WeightedValidators) NewCommitInfo(r *rand.Rand) comet.CommitInfo { + if len(v) == 0 { + return comet.CommitInfo{Votes: make([]comet.VoteInfo, 0)} + } + if r.Intn(10) == 0 { + v[r.Intn(len(v))].Offline = r.Intn(2) == 0 + } + votes := make([]comet.VoteInfo, 0, len(v)) + for i := range v { + if v[i].Offline { + continue + } + votes = append(votes, comet.VoteInfo{ + Validator: comet.Validator{Address: v[i].Address, Power: v[i].Power}, + BlockIDFlag: comet.BlockIDFlagCommit, + }) + } + return comet.CommitInfo{Round: int32(r.Uint32()), Votes: votes} +} + +func (v WeightedValidators) TotalPower() int64 { + var r int64 + for _, val := range v { + r += val.Power + } + return r +} diff --git a/simsx/v2/valset_history.go b/simsx/v2/valset_history.go new file mode 100644 index 0000000000..9c05978ef9 --- /dev/null +++ b/simsx/v2/valset_history.go @@ -0,0 +1,78 @@ +package v2 + +import ( + "math/rand" + "slices" + "time" + + "cosmossdk.io/core/comet" + + "github.com/cosmos/cosmos-sdk/simsx" +) + +type historicValSet struct { + blockTime time.Time + vals WeightedValidators +} +type ValSetHistory struct { + maxElements int + blockOffset int + vals []historicValSet +} + +func NewValSetHistory(maxElements int) *ValSetHistory { + return &ValSetHistory{ + maxElements: maxElements, + blockOffset: 1, // start at height 1 + vals: make([]historicValSet, 0, maxElements), + } +} + +func (h *ValSetHistory) Add(blockTime time.Time, vals WeightedValidators) { + vals = slices.DeleteFunc(vals, func(validator WeightedValidator) bool { + return validator.Power == 0 + }) + slices.SortFunc(vals, func(a, b WeightedValidator) int { + return b.Compare(a) + }) + newEntry := historicValSet{blockTime: blockTime, vals: vals} + if len(h.vals) >= h.maxElements { + h.vals = append(h.vals[1:], newEntry) + h.blockOffset++ + return + } + h.vals = append(h.vals, newEntry) +} + +// MissBehaviour determines if a random validator misbehaves, creating and returning evidence for duplicate voting. +// Returns a slice of comet.Evidence if misbehavior is detected; otherwise, returns nil. +// Has a 1% chance of generating evidence for a validator's misbehavior. +// Recursively checks for other misbehavior instances and combines their evidence if any. +// Utilizes a random generator to select a validator and evidence-related attributes. +func (h *ValSetHistory) MissBehaviour(r *rand.Rand) []comet.Evidence { + if r.Intn(100) != 0 { // 1% chance + return nil + } + n := r.Intn(len(h.vals)) + badVal := simsx.OneOf(r, h.vals[n].vals) + evidence := comet.Evidence{ + Type: comet.DuplicateVote, + Validator: comet.Validator{Address: badVal.Address, Power: badVal.Power}, + Height: int64(h.blockOffset + n), + Time: h.vals[n].blockTime, + TotalVotingPower: h.vals[n].vals.TotalPower(), + } + if otherEvidence := h.MissBehaviour(r); otherEvidence != nil { + return append([]comet.Evidence{evidence}, otherEvidence...) + } + return []comet.Evidence{evidence} +} + +func (h *ValSetHistory) SetMaxHistory(v int) { + h.maxElements = v + if len(h.vals) > h.maxElements { + diff := len(h.vals) - h.maxElements + h.vals = h.vals[diff:] + h.blockOffset += diff + } +} diff --git a/testutil/sims/state_helpers.go b/testutil/sims/state_helpers.go index 76d06b8959..8e229891c7 100644 --- a/testutil/sims/state_helpers.go +++ b/testutil/sims/state_helpers.go @@ -212,6 +212,7 @@ func AppStateRandomizedFn( // AppStateFromGenesisFileFn util function to generate the genesis AppState // from a genesis.json file. +// Deprecated: the private keys are not matching the accounts read from app state func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile string) (genutiltypes.AppGenesis, []simtypes.Account, error) { file, err := os.Open(filepath.Clean(genesisFile)) if err != nil { @@ -233,6 +234,8 @@ func AppStateFromGenesisFileFn(_ io.Reader, cdc codec.JSONCodec, genesisFile str return *genesis, newAccs, nil } +// AccountsFromAppState +// Deprecated: the private keys are not matching the accounts read from app state func AccountsFromAppState(cdc codec.JSONCodec, appStateJSON json.RawMessage) ([]simtypes.Account, error) { var appState map[string]json.RawMessage if err := json.Unmarshal(appStateJSON, &appState); err != nil { diff --git a/testutil/sims/tx_helpers.go b/testutil/sims/tx_helpers.go index 9f71f8aadb..ac5685de2c 100644 --- a/testutil/sims/tx_helpers.go +++ b/testutil/sims/tx_helpers.go @@ -22,7 +22,16 @@ import ( ) // GenSignedMockTx generates a signed mock transaction. -func GenSignedMockTx(r *rand.Rand, txConfig client.TxConfig, msgs []sdk.Msg, feeAmt sdk.Coins, gas uint64, chainID string, accNums, accSeqs []uint64, priv ...cryptotypes.PrivKey) (sdk.Tx, error) { +func GenSignedMockTx( + r *rand.Rand, + txConfig client.TxConfig, + msgs []sdk.Msg, + feeAmt sdk.Coins, + gas uint64, + chainID string, + accNums, accSeqs []uint64, + priv ...cryptotypes.PrivKey, +) (sdk.Tx, error) { sigs := make([]signing.SignatureV2, len(priv)) // create a random length memo diff --git a/testutil/testdata/tx.go b/testutil/testdata/tx.go index 6de23c818b..cf2a93c063 100644 --- a/testutil/testdata/tx.go +++ b/testutil/testdata/tx.go @@ -1,12 +1,13 @@ package testdata import ( - "cosmossdk.io/math" "testing" "gotest.tools/v3/assert" "pgregory.net/rapid" + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1" diff --git a/x/gov/simulation/msg_factory.go b/x/gov/simulation/msg_factory.go index b1267a9ea2..efe1b39790 100644 --- a/x/gov/simulation/msg_factory.go +++ b/x/gov/simulation/msg_factory.go @@ -131,8 +131,7 @@ func submitProposalWithVotesScheduled( proposalMsgs ...sdk.Msg, ) ([]simsx.SimAccount, *v1.MsgSubmitProposal) { r := testData.Rand() - expedited := true - // expedited := r.Bool() + expedited := r.Bool() params := must(k.Params.Get(ctx)) minDeposits := params.MinDeposit if expedited { diff --git a/x/nft/simulation/msg_factory.go b/x/nft/simulation/msg_factory.go index 14db2666de..8ec1c63ce6 100644 --- a/x/nft/simulation/msg_factory.go +++ b/x/nft/simulation/msg_factory.go @@ -14,7 +14,9 @@ func MsgSendFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*nft.MsgSend] { return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *nft.MsgSend) { from := testData.AnyAccount(reporter, simsx.WithSpendableBalance()) to := testData.AnyAccount(reporter, simsx.ExcludeAccounts(from)) - + if reporter.IsSkipped() { + return nil, nil + } n, err := randNFT(ctx, testData.Rand(), k, from.Address) if err != nil { reporter.Skip(err.Error()) diff --git a/x/staking/simulation/msg_factory.go b/x/staking/simulation/msg_factory.go index 31c35cddd5..a758474e1d 100644 --- a/x/staking/simulation/msg_factory.go +++ b/x/staking/simulation/msg_factory.go @@ -313,21 +313,11 @@ func MsgRotateConsPubKeyFactory(k *keeper.Keeper) simsx.SimMsgFactoryFn[*types.M return nil, nil } // check whether the new cons key associated with another validator - newConsAddr := sdk.ConsAddress(otherAccount.ConsKey.PubKey().Address()) - - if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil { - reporter.Skip("cons key already used") + assertKeyUnused(ctx, reporter, k, otherAccount.ConsKey.PubKey()) + if reporter.IsSkipped() { return nil, nil } msg := must(types.NewMsgRotateConsPubKey(val.GetOperator(), otherAccount.ConsKey.PubKey())) - - // check if there's another key rotation for this same key in the same block - for _, r := range must(k.GetBlockConsPubKeyRotationHistory(ctx)) { - if r.NewConsPubkey.Compare(msg.NewPubkey) == 0 { - reporter.Skip("cons key already used in this block") - return nil, nil - } - } return []simsx.SimAccount{valOper}, msg } } @@ -363,6 +353,16 @@ func randomValidator(ctx context.Context, reporter simsx.SimulationReporter, k * // skips execution if there's another key rotation for the same key in the same block func assertKeyUnused(ctx context.Context, reporter simsx.SimulationReporter, k *keeper.Keeper, newPubKey cryptotypes.PubKey) { + newConsAddr := sdk.ConsAddress(newPubKey.Address()) + if rotatedTo, _ := k.ConsAddrToValidatorIdentifierMap.Get(ctx, newConsAddr); rotatedTo != nil { + reporter.Skip("consensus key already used") + return + } + if _, err := k.GetValidatorByConsAddr(ctx, newConsAddr); err == nil { + reporter.Skip("cons key already used") + return + } + allRotations, err := k.GetBlockConsPubKeyRotationHistory(ctx) if err != nil { reporter.Skipf("cannot get block cons key rotation history: %s", err.Error())