diff --git a/.github/pr_labeler.yml b/.github/pr_labeler.yml index 76cc7bc59c21..4776f36cbda7 100644 --- a/.github/pr_labeler.yml +++ b/.github/pr_labeler.yml @@ -1,3 +1,25 @@ +"C:CLI": + - client/**/* + - x/*/client/**/* +"C:Confix": + - tools/confix/**/* +"C:Cosmovisor": + - tools/cosmovisor/**/* +"C:Hubl": + - tools/hubl/**/* +"C:Keys": + - client/keys/**/* +"C:Simulations": + - x/simulation/**/* + - x/*/simulation/**/* +"C:Store": + - store/**/* +"C:collections": + - collections/**/* +"C:log": + - log/* +"C:orm": + - orm/**/* "C:x/accounts": - x/accounts/**/* "C:x/auth": @@ -6,6 +28,10 @@ - x/authz/**/* "C:x/bank": - x/bank/**/* +"C:x/circuit": + - x/circuit/**/* +"C:x/consensus": + - x/consensus/**/* "C:x/crisis": - x/crisis/**/* "C:x/distribution": @@ -26,33 +52,18 @@ - x/nft/**/* "C:x/params": - x/params/**/* -"C:Simulations": - - x/simulation/**/* - - x/*/simulation/**/* +"C:x/protocolpool": + - x/protocolpool/**/* "C:x/slashing": - x/slashing/**/* "C:x/staking": - x/staking/**/* -"C:x/upgrade": - - x/upgrade/**/* -"C:x/consensus": - - x/consensus/**/* -"C:x/circuit": - - x/circuit/**/* -"C:x/protocolpool": - - x/protocolpool/**/* "C:x/tx": - x/tx/**/* -"C:collections": - - collections/**/* -"C:Cosmovisor": - - tools/cosmovisor/**/* -"C:Confix": - - tools/confix/**/* -"C:Hubl": - - tools/hubl/**/* -"C:Keys": - - client/keys/**/* +"C:x/upgrade": + - x/upgrade/**/* +"Type: ADR": + - docs/architecture/**/* "Type: Build": - Makefile - Dockerfile @@ -63,14 +74,3 @@ - buf.yaml - .mergify.yml - .golangci.yml -"C:CLI": - - client/**/* - - x/*/client/**/* -"Type: ADR": - - docs/architecture/**/* -"C:Store": - - store/**/* -"C:orm": - - orm/**/* -"C:log": - - log/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d23881ed7575..7f2e1f418f68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 22a64763b230..e7c8ce4544f8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/dependabot-update-all.yml b/.github/workflows/dependabot-update-all.yml index 129ae63fb55c..0bf09c8b06d1 100644 --- a/.github/workflows/dependabot-update-all.yml +++ b/.github/workflows/dependabot-update-all.yml @@ -15,7 +15,7 @@ jobs: ref: ${{ github.event.pull_request.head.ref }} # Secret to be added in the repo under Settings > Secrets > Dependabot token: ${{ secrets.PRBOT_PAT }} - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/dependencies-review.yml b/.github/workflows/dependencies-review.yml index c0e4c79e6378..d7a736ff8d88 100644 --- a/.github/workflows/dependencies-review.yml +++ b/.github/workflows/dependencies-review.yml @@ -12,7 +12,7 @@ jobs: steps: - name: "Checkout Repository" uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 532840fccb69..674ffb9a0575 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/release-confix.yml b/.github/workflows/release-confix.yml index 60e6bf53ca4b..d284fd5551e0 100644 --- a/.github/workflows/release-confix.yml +++ b/.github/workflows/release-confix.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/release-cosmovisor.yml b/.github/workflows/release-cosmovisor.yml index 8b7d0335bcac..38a58b4a7201 100644 --- a/.github/workflows/release-cosmovisor.yml +++ b/.github/workflows/release-cosmovisor.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb031d3ec8e8..d0d88133bd62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/sims-047.yml b/.github/workflows/sims-047.yml index 28e7f97ced5a..f0bef153d0bc 100644 --- a/.github/workflows/sims-047.yml +++ b/.github/workflows/sims-047.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.47.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest needs: build steps: - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.47.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -69,7 +69,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.47.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.47.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/sims-050.yml b/.github/workflows/sims-050.yml index d3f28a078277..ae236a5c9d78 100644 --- a/.github/workflows/sims-050.yml +++ b/.github/workflows/sims-050.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.50.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest needs: build steps: - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.50.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -69,7 +69,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.50.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v4 with: ref: "release/v0.50.x" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/sims-nightly.yml b/.github/workflows/sims-nightly.yml index a34889eeec3c..f28e6b9d3627 100644 --- a/.github/workflows/sims-nightly.yml +++ b/.github/workflows/sims-nightly.yml @@ -20,7 +20,7 @@ jobs: contents: none runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/sims.yml b/.github/workflows/sims.yml index e54e83e7bce0..896b9106d83b 100644 --- a/.github/workflows/sims.yml +++ b/.github/workflows/sims.yml @@ -19,7 +19,7 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip-sims')" steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -37,7 +37,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -54,7 +54,7 @@ jobs: needs: [build] steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -72,7 +72,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/starship-tests.yml b/.github/workflows/starship-tests.yml index 0b1587a528b6..9533aaba0f9a 100644 --- a/.github/workflows/starship-tests.yml +++ b/.github/workflows/starship-tests.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddbf1b6a00a0..0aa767ef7804 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -52,7 +52,7 @@ jobs: part: ["00", "01", "02", "03"] steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -86,7 +86,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -117,7 +117,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -195,7 +195,7 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -225,7 +225,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -258,7 +258,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -289,7 +289,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -320,7 +320,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -349,7 +349,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -380,7 +380,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -411,7 +411,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -447,7 +447,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -478,7 +478,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -509,7 +509,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -540,7 +540,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -571,7 +571,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -604,7 +604,7 @@ jobs: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -635,7 +635,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.20" check-latest: true @@ -673,7 +673,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -704,7 +704,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -735,7 +735,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -766,7 +766,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -797,7 +797,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -828,7 +828,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -859,7 +859,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -890,7 +890,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -920,7 +920,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -950,7 +950,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -980,7 +980,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1010,7 +1010,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1041,7 +1041,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1072,7 +1072,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1103,7 +1103,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1134,7 +1134,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1165,7 +1165,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true @@ -1196,7 +1196,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "1.21" check-latest: true diff --git a/CHANGELOG.md b/CHANGELOG.md index db0a67a490ef..276e1a785646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,7 +84,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (simulation) [#17911](https://github.com/cosmos/cosmos-sdk/pull/17911) Fix all problems with executing command `make test-sim-custom-genesis-fast` for simulation test. * (simulation) [#18196](https://github.com/cosmos/cosmos-sdk/pull/18196) Fix the problem of `validator set is empty after InitGenesis` in simulation test. * (baseapp) [#18551](https://github.com/cosmos/cosmos-sdk/pull/18551) Fix SelectTxForProposal the calculation method of tx bytes size is inconsistent with CometBFT -* (baseapp) [#18653](https://github.com/cosmos/cosmos-sdk/pull/18653) Fix protocompat hybrid handler merging on gogoproto handlers for messages with custom types. +* (baseapp) [#18654](https://github.com/cosmos/cosmos-sdk/pull/18654) Fixes an issue in which gogoproto.Merge does not work with gogoproto messages with custom types. ### API Breaking Changes @@ -186,6 +186,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### State Machine Breaking +* (baseapp) [#18627](https://github.com/cosmos/cosmos-sdk/pull/18627) Post handlers are run on non successful transaction executions too. * (x/upgrade) [#16244](https://github.com/cosmos/cosmos-sdk/pull/16244) Upgrade module no longer stores the app version but gets and sets the app version stored in the `ParamStore` of baseapp. * (x/staking) [#17655](https://github.com/cosmos/cosmos-sdk/pull/17655) `HistoricalInfo` was replaced with `HistoricalRecord`, it removes the validator set and comet header and only keep what is needed for IBC. diff --git a/UPGRADING.md b/UPGRADING.md index 62fc97d93420..5e52d8b56ba6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -346,7 +346,7 @@ User manually wiring their chain need to add the logger argument when creating t #### Module Basics -Previously, the `ModuleBasics` was a global variable that was used to register all modules's `AppModuleBasic` implementation. +Previously, the `ModuleBasics` was a global variable that was used to register all modules' `AppModuleBasic` implementation. The global variable has been removed and the basic module manager can be now created from the module manager. This is automatically done for `depinject` / app v2 users, however for supplying different app module implementation, pass them via `depinject.Supply` in the main `AppConfig` (`app_config.go`): @@ -365,7 +365,7 @@ depinject.Supply( ) ``` -Users manually wiring their chain need to use the new `module.NewBasicManagerFromManager` function, after the module manager creation, and pass a `map[string]module.AppModuleBasic` as argument for optionally overridding some module's `AppModuleBasic`. +Users manually wiring their chain need to use the new `module.NewBasicManagerFromManager` function, after the module manager creation, and pass a `map[string]module.AppModuleBasic` as argument for optionally overriding some module's `AppModuleBasic`. #### AutoCLI @@ -493,7 +493,7 @@ To learn more see the [docs](https://docs.cosmos.network/main/learn/advanced/tra * Messages no longer need to implement the `LegacyMsg` interface and implementations of `GetSignBytes` can be deleted. Because of this change, global legacy Amino codec definitions and their registration in `init()` can safely be removed as well. -* The `AppModuleBasic` interface has been simplifed. Defining `GetTxCmd() *cobra.Command` and `GetQueryCmd() *cobra.Command` is no longer required. The module manager detects when module commands are defined. If AutoCLI is enabled, `EnhanceRootCommand()` will add the auto-generated commands to the root command, unless a custom module command is defined and register that one instead. +* The `AppModuleBasic` interface has been simplified. Defining `GetTxCmd() *cobra.Command` and `GetQueryCmd() *cobra.Command` is no longer required. The module manager detects when module commands are defined. If AutoCLI is enabled, `EnhanceRootCommand()` will add the auto-generated commands to the root command, unless a custom module command is defined and register that one instead. * The following modules' `Keeper` methods now take in a `context.Context` instead of `sdk.Context`. Any module that has an interfaces for them (like "expected keepers") will need to update and re-generate mocks if needed: @@ -800,7 +800,7 @@ In case a module does not follow the standard message path, (e.g. IBC), it is ad #### `x/params` The `params` module was deprecated since v0.46. The Cosmos SDK has migrated away from `x/params` for its own modules. -Cosmos SDK modules now store their parameters directly in its repective modules. +Cosmos SDK modules now store their parameters directly in its respective modules. The `params` module will be removed in `v0.50`, as mentioned [in v0.46 release](https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/UPGRADING.md#xparams). It is strongly encouraged to migrate away from `x/params` before `v0.50`. When performing a chain migration, the params table must be initizalied manually. This was done in the modules keepers in previous versions. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 7fea57304593..29f1839af001 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -938,24 +938,25 @@ func (app *BaseApp) runTx(mode execMode, txBytes []byte) (gInfo sdk.GasInfo, res if err == nil { result, err = app.runMsgs(runMsgCtx, msgs, msgsV2, mode) } - if err == nil { - // Run optional postHandlers. - // - // Note: If the postHandler fails, we also revert the runMsgs state. - if app.postHandler != nil { - // The runMsgCtx context currently contains events emitted by the ante handler. - // We clear this to correctly order events without duplicates. - // Note that the state is still preserved. - postCtx := runMsgCtx.WithEventManager(sdk.NewEventManager()) - - newCtx, err := app.postHandler(postCtx, tx, mode == execModeSimulate, err == nil) - if err != nil { - return gInfo, nil, anteEvents, err - } - result.Events = append(result.Events, newCtx.EventManager().ABCIEvents()...) + // Run optional postHandlers (should run regardless of the execution result). + // + // Note: If the postHandler fails, we also revert the runMsgs state. + if app.postHandler != nil { + // The runMsgCtx context currently contains events emitted by the ante handler. + // We clear this to correctly order events without duplicates. + // Note that the state is still preserved. + postCtx := runMsgCtx.WithEventManager(sdk.NewEventManager()) + + newCtx, err := app.postHandler(postCtx, tx, mode == execModeSimulate, err == nil) + if err != nil { + return gInfo, nil, anteEvents, err } + result.Events = append(result.Events, newCtx.EventManager().ABCIEvents()...) + } + + if err == nil { if mode == execModeFinalize { // When block gas exceeds, it'll panic and won't commit the cached store. consumeBlockGas() diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index c769f773c3d2..67141a5e3002 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -594,6 +594,53 @@ func TestBaseAppAnteHandler(t *testing.T) { require.NoError(t, err) } +func TestBaseAppPostHandler(t *testing.T) { + postHandlerRun := false + anteOpt := func(bapp *baseapp.BaseApp) { + bapp.SetPostHandler(func(ctx sdk.Context, tx sdk.Tx, simulate, success bool) (newCtx sdk.Context, err error) { + postHandlerRun = true + return ctx, nil + }) + } + + suite := NewBaseAppSuite(t, anteOpt) + + baseapptestutil.RegisterCounterServer(suite.baseApp.MsgServiceRouter(), CounterServerImpl{t, capKey1, []byte("foo")}) + + _, err := suite.baseApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: &cmtproto.ConsensusParams{}, + }) + require.NoError(t, err) + + // execute a tx that will fail ante handler execution + // + // NOTE: State should not be mutated here. This will be implicitly checked by + // the next txs ante handler execution (anteHandlerTxTest). + tx := newTxCounter(t, suite.txConfig, 0, 0) + txBytes, err := suite.txConfig.TxEncoder()(tx) + require.NoError(t, err) + + res, err := suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1, Txs: [][]byte{txBytes}}) + require.NoError(t, err) + require.Empty(t, res.Events) + require.True(t, res.TxResults[0].IsOK(), fmt.Sprintf("%v", res)) + + // PostHandler runs on successful message execution + require.True(t, postHandlerRun) + + // It should also run on failed message execution + postHandlerRun = false + tx = setFailOnHandler(t, suite.txConfig, tx, true) + txBytes, err = suite.txConfig.TxEncoder()(tx) + require.NoError(t, err) + res, err = suite.baseApp.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1, Txs: [][]byte{txBytes}}) + require.NoError(t, err) + require.Empty(t, res.Events) + require.False(t, res.TxResults[0].IsOK(), fmt.Sprintf("%v", res)) + + require.True(t, postHandlerRun) +} + // Test and ensure that invalid block heights always cause errors. // See issues: // - https://github.com/cosmos/cosmos-sdk/issues/11220 diff --git a/client/v2/README.md b/client/v2/README.md index 96c5359ff62a..5830667f644b 100644 --- a/client/v2/README.md +++ b/client/v2/README.md @@ -37,7 +37,7 @@ It is possible to customize the generation of transactions and queries by defini Here are the steps to use AutoCLI: 1. Ensure your app's modules implements the `appmodule.AppModule` interface. -2. (optional) Configure how behave `autocli` command generation, by implementing the `func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions` method on the module. +2. (optional) Configure how to behave as `autocli` command generation, by implementing the `func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions` method on the module. 3. Use the `autocli.AppOptions` struct to specify the modules you defined. If you are using `depinject` / app v2, it can automatically create an instance of `autocli.AppOptions` based on your app's configuration. 4. Use the `EnhanceRootCommand()` method provided by `autocli` to add the CLI commands for the specified modules to your root command. @@ -211,7 +211,7 @@ https://github.com/cosmos/cosmos-sdk/blob/main/client/grpc/cmtservice/autocli.go ## Summary -`autocli` let you generate CLI to your Cosmos SDK-based applications without any cobra boilerplate. It allows you to easily generate CLI commands and flags from your protobuf messages, and provides many options for customising the behavior of your CLI application. +`autocli` lets you generate CLI to your Cosmos SDK-based applications without any cobra boilerplate. It allows you to easily generate CLI commands and flags from your protobuf messages, and provides many options for customising the behavior of your CLI application. To further enhance your CLI experience with Cosmos SDK-based blockchains, you can use `hubl`. `hubl` is a tool that allows you to query any Cosmos SDK-based blockchain using the new AutoCLI feature of the Cosmos SDK. With `hubl`, you can easily configure a new chain and query modules with just a few simple commands. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index ae22cc160418..6ff313fb0be2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -60,6 +60,7 @@ When writing ADRs, follow the same best practices for writing RFCs. When writing * [ADR 058: Auto-Generated CLI](./adr-058-auto-generated-cli.md) * [ADR 060: ABCI 1.0 (Phase I)](adr-060-abci-1.0.md) * [ADR 061: Liquid Staking](./adr-061-liquid-staking.md) +* [ADR 070: Un-Ordered Transaction Inclusion](./adr-070-unordered-account.md) ### Proposed diff --git a/docs/architecture/adr-007-specialization-groups.md b/docs/architecture/adr-007-specialization-groups.md index bafcc697bf2c..dd5617a49d09 100644 --- a/docs/architecture/adr-007-specialization-groups.md +++ b/docs/architecture/adr-007-specialization-groups.md @@ -10,7 +10,7 @@ This idea was first conceived of in order to fulfill the use case of the creation of a decentralized Computer Emergency Response Team (dCERT), whose members would be elected by a governing community and would fulfill the role of coordinating the community under emergency situations. This thinking -can be further abstracted into the conception of "blockchain specialization +can be further abstracted into the concept of "blockchain specialization groups". The creation of these groups are the beginning of specialization capabilities @@ -44,7 +44,7 @@ A specialization group can be broadly broken down into the following functions * Individual compensation for all constituents of a group from the greater community -Membership admittance to a specialization group could take place over a wide +Membership admission to a specialization group could take place over a wide variety of mechanisms. The most obvious example is through a general vote among the entire community, however in certain systems a community may want to allow the members already in a specialization group to internally elect new members, diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md new file mode 100644 index 000000000000..814193cef668 --- /dev/null +++ b/docs/architecture/adr-070-unordered-account.md @@ -0,0 +1,260 @@ +# ADR 070: Un-Ordered Transaction Inclusion + +## Changelog + +* Dec 4, 2023: Initial Draft + +## Status + +ACCEPTED + +## Abstract + +We propose a way to do replay-attack protection without enforcing the order of +transactions, without requiring the use of nonces. In this way, we can support +un-ordered transaction inclusion. + +## Context + +As of today, the nonce value (account sequence number) prevents replay-attack and +ensures the transactions from the same sender are included into blocks and executed +in sequential order. However it makes it tricky to send many transactions from the +same sender concurrently in a reliable way. IBC relayer and crypto exchanges are +typical examples of such use cases. + +## Decision + +We propose to add a boolean field `unordered` to transaction body to mark "un-ordered" +transactions. + +Un-ordered transactions will bypass the nonce rules and follow the rules described +below instead, in contrary, the default ordered transactions are not impacted by +this proposal, they'll follow the nonce rules the same as before. + +When an un-ordered transaction is included into a block, the transaction hash is +recorded in a dictionary. New transactions are checked against this dictionary for +duplicates, and to prevent the dictionary grow indefinitely, the transaction must +specify `timeout_height` for expiration, so it's safe to removed it from the +dictionary after it's expired. + +The dictionary can be simply implemented as an in-memory golang map, a preliminary +analysis shows that the memory consumption won't be too big, for example `32M = 32 * 1024 * 1024` +can support 1024 blocks where each block contains 1024 unordered transactions. For +safety, we should limit the range of `timeout_height` to prevent very long expiration, +and limit the size of the dictionary. + +### Transaction Format + +```protobuf +message TxBody { + ... + + bool unordered = 4; +} +``` + +### `DedupTxHashManager` + +```golang +const PurgeLoopSleepMS = 500 + +// DedupTxHashManager contains the tx hash dictionary for duplicates checking, +// and expire them when block number progresses. +type DedupTxHashManager struct { + mutex sync.RWMutex + // tx hash -> expire block number + // for duplicates checking and expiration + hashes map[TxHash]uint64 + // channel to receive latest block numbers + blockCh chan uint64 +} + +func NewDedupTxHashManager() *DedupTxHashManager { + m := &DedupTxHashManager{ + hashes: make(map[TxHash]uint64), + blockCh: make(ch *uint64, 16), + } + go m.purgeLoop() + return m +} + +func (dtm *DedupTxHashManager) Close() error { + close(dtm.blockCh) + dtm.blockCh = nil + return nil +} + +func (dtm *DedupTxHashManager) Contains(hash TxHash) (ok bool) { + dtm.mutex.RLock() + defer dtm.mutex.RUnlock() + + _, ok = dtm.hashes[hash] + return +} + +func (dtm *DedupTxHashManager) Size() int { + dtm.mutex.RLock() + defer dtm.mutex.RUnlock() + + return len(dtm.hashes) +} + +func (dtm *DedupTxHashManager) Add(hash TxHash, expire uint64) (ok bool) { + dtm.mutex.Lock() + defer dtm.mutex.Unlock() + + dtm.hashes[hash] = expire + return +} + +// OnNewBlock send the latest block number to the background purge loop, +// it should be called in abci commit event. +func (dtm *DedupTxHashManager) OnNewBlock(blockNumber uint64) { + dtm.blockCh <- &blockNumber +} + +// purgeLoop removes expired tx hashes at background +func (dtm *DedupTxHashManager) purgeLoop() error { + for { + blocks := channelBatchRecv(dtm.blockCh) + if len(blocks) == 0 { + // channel closed + break + } + + latest := *blocks[len(blocks)-1] + hashes := dtm.expired(latest) + if len(hashes) > 0 { + dtm.purge(hashes) + } + + // avoid burning cpu in catching up phase + time.Sleep(PurgeLoopSleepMS * time.Millisecond) + } +} + +// expired find out expired tx hashes based on latest block number +func (dtm *DedupTxHashManager) expired(block uint64) []TxHash { + dtm.mutex.RLock() + defer dtm.mutex.RUnlock() + + var result []TxHash + for h, expire := range dtm.hashes { + if block > expire { + result = append(result, h) + } + } + return result +} + +func (dtm *DedupTxHashManager) purge(hashes []TxHash) { + dtm.mutex.Lock() + defer dtm.mutex.Unlock() + + for _, hash := range hashes { + delete(dtm.hashes, hash) + } +} + +// channelBatchRecv try to exhaust the channel buffer when it's not empty, +// and block when it's empty. +func channelBatchRecv[T any](ch <-chan *T) []*T { + item := <-ch // block if channel is empty + if item == nil { + // channel is closed + return nil + } + + remaining := len(ch) + result := make([]*T, 0, remaining+1) + result = append(result, item) + for i := 0; i < remaining; i++ { + result = append(result, <-ch) + } + + return result +} +``` + +### Ante Handlers + +Bypass the nonce decorator for un-ordered transactions. + +```golang +func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if tx.UnOrdered() { + return next(ctx, tx, simulate) + } + + // the previous logic +} +``` + +A decorator for the new logic. + +```golang +type TxHash [32]byte + +const ( + // MaxUnOrderedTTL defines the maximum ttl an un-order tx can set + MaxUnOrderedTTL = 1024 +) + +type DedupTxDecorator struct { + m *DedupTxHashManager +} + +func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // only apply to un-ordered transactions + if !tx.UnOrdered() { + return next(ctx, tx, simulate) + } + + if tx.TimeoutHeight() == 0 { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "unordered tx must set timeout-height") + } + + if tx.TimeoutHeight() > ctx.BlockHeight() + MaxUnOrderedTTL { + return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "unordered tx ttl exceeds %d", MaxUnOrderedTTL) + } + + // check for duplicates + if dtd.m.Contains(tx.Hash()) { + return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "tx is duplicated") + } + + if !ctx.IsCheckTx() { + // a new tx included in the block, add the hash to the dictionary + dtd.m.Add(tx.Hash(), tx.TimeoutHeight()) + } + + return next(ctx, tx, simulate) +} +``` + +### `OnNewBlock` + +Wire the `OnNewBlock` method of `DedupTxHashManager` into the BaseApp's ABCI Commit event. + +### Start Up + +On start up, the node needs to re-fill the tx hash dictionary of `DedupTxHashManager` +by scanning `MaxUnOrderedTTL` number of historical blocks for existing un-expired +un-ordered transactions. + +An alternative design is to store the tx hash dictionary in kv store, then no need +to warm up on start up. + +## Consequences + +### Positive + +* Support un-ordered and concurrent transaction inclusion. + +### Negative + +* Start up overhead to scan historical blocks. + +## References + +* https://github.com/cosmos/cosmos-sdk/issues/13009 diff --git a/docs/spec/SPEC_MODULE.md b/docs/spec/SPEC_MODULE.md index 1b5e5d5dc65f..275d17a5c42e 100644 --- a/docs/spec/SPEC_MODULE.md +++ b/docs/spec/SPEC_MODULE.md @@ -24,7 +24,7 @@ specifications for modules. The following list is nonbinding and all sections ar * `# {Module Name}` - overview of the module * `## Concepts` - describe specialized concepts and definitions used throughout the spec -* `## State` - specify and describe structures expected to marshalled into the store, and their keys +* `## State` - specify and describe structures expected to be marshalled into the store, and their keys * `## State Transitions` - standard state transition operations triggered by hooks, messages, etc. * `## Messages` - specify message structure(s) and expected state machine behaviour(s) * `## Begin Block` - specify any begin-block operations diff --git a/docs/spec/SPEC_STANDARD.md b/docs/spec/SPEC_STANDARD.md index 3608b3654c0b..f6dc07b852f5 100644 --- a/docs/spec/SPEC_STANDARD.md +++ b/docs/spec/SPEC_STANDARD.md @@ -42,7 +42,7 @@ This section should include an assumptions sub-section if any, the mandatory pro This is the main section of the document, and should contain protocol documentation, design rationale, required references, and technical details where appropriate. The section may have any or all of the following sub-sections, as appropriate to the particular specification. The API sub-section is especially encouraged when appropriate. -* *API* - A detailed description of the features's API. +* *API* - A detailed description of the feature's API. * *Technical Details* - All technical details including syntax, diagrams, semantics, protocols, data structures, algorithms, and pseudocode as appropriate. The technical specification should be detailed enough such that separate correct implementations of the specification without knowledge of each other are compatible. * *Backwards Compatibility* - A discussion of compatibility (or lack thereof) with previous feature or protocol versions. * *Known Issues* - A list of known issues. This sub-section is specially important for specifications of already in-use features. diff --git a/docs/spec/store/README.md b/docs/spec/store/README.md index c53d69c67d03..d9d35e7d465c 100644 --- a/docs/spec/store/README.md +++ b/docs/spec/store/README.md @@ -29,7 +29,7 @@ with, which also provides the basis of most state storage and commitment operati is the `KVStore`. The `KVStore` interface provides basic CRUD abilities and prefix-based iteration, including reverse iteration. -Typically, each module has it's own dedicated `KVStore` instance, which it can +Typically, each module has its own dedicated `KVStore` instance, which it can get access to via the `sdk.Context` and the use of a pointer-based named key -- `KVStoreKey`. The `KVStoreKey` provides pseudo-OCAP. How a exactly a `KVStoreKey` maps to a `KVStore` will be illustrated below through the `CommitMultiStore`. @@ -42,7 +42,7 @@ until `Commit()` is called on the `CommitMultiStore`. ### `CommitMultiStore` -The `CommitMultiStore` interface exposes the the top-level interface that is used +The `CommitMultiStore` interface exposes the top-level interface that is used to manage state commitment and storage by an SDK application and abstracts the concept of multiple `KVStore`s which are used by multiple modules. Specifically, it supports the following high-level primitives: @@ -53,7 +53,7 @@ it supports the following high-level primitives: * Allows for loading state storage at a particular height/version in the past to provide current head and historical queries. * Provides the ability to rollback state to a previous height/version. -* Provides the ability to to load state storage at a particular height/version +* Provides the ability to load state storage at a particular height/version while also performing store upgrades, which are used during live hard-fork application state migrations. * Provides the ability to commit all current accumulated state to disk and performs diff --git a/store/changeset.go b/store/changeset.go index b9ceaa50764f..6982344375af 100644 --- a/store/changeset.go +++ b/store/changeset.go @@ -4,8 +4,9 @@ package store // track writes. Deletion can be denoted by a nil value or explicitly by the // Delete field. type KVPair struct { - Key []byte - Value []byte + Key []byte + Value []byte + StoreKey string // Optional for snapshot restore } type KVPairs []KVPair diff --git a/store/commit_info.go b/store/commit_info.go index 103118ece02f..1bfad27e3eef 100644 --- a/store/commit_info.go +++ b/store/commit_info.go @@ -8,14 +8,6 @@ import ( ) type ( - // CommitHeader defines the interface for a block header that can be provided - // to a MultiStore upon Commit. This should be optional and used to facilitate - // time-based queries only. - CommitHeader interface { - GetTime() time.Time - GetHeight() uint64 - } - // CommitInfo defines commit information used by the multi-store when committing // a version/height. CommitInfo struct { @@ -80,3 +72,7 @@ func (m *CommitInfo) GetVersion() uint64 { func (cid CommitID) String() string { return fmt.Sprintf("CommitID{%v:%X}", cid.Hash, cid.Version) } + +func (cid CommitID) IsZero() bool { + return cid.Version == 0 && len(cid.Hash) == 0 +} diff --git a/store/commitment/iavl/exporter.go b/store/commitment/iavl/exporter.go new file mode 100644 index 000000000000..20f00d1a1722 --- /dev/null +++ b/store/commitment/iavl/exporter.go @@ -0,0 +1,40 @@ +package iavl + +import ( + "errors" + + "github.com/cosmos/iavl" + + "cosmossdk.io/store/v2/commitment" + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" +) + +// Exporter is a wrapper around iavl.Exporter. +type Exporter struct { + exporter *iavl.Exporter +} + +// Next returns the next item in the exporter. +func (e *Exporter) Next() (*snapshotstypes.SnapshotIAVLItem, error) { + item, err := e.exporter.Next() + if err != nil { + if errors.Is(err, iavl.ErrorExportDone) { + return nil, commitment.ErrorExportDone + } + return nil, err + } + + return &snapshotstypes.SnapshotIAVLItem{ + Key: item.Key, + Value: item.Value, + Version: item.Version, + Height: int32(item.Height), + }, nil +} + +// Close closes the exporter. +func (e *Exporter) Close() error { + e.exporter.Close() + + return nil +} diff --git a/store/commitment/iavl/importer.go b/store/commitment/iavl/importer.go new file mode 100644 index 000000000000..6f1b0eedf21f --- /dev/null +++ b/store/commitment/iavl/importer.go @@ -0,0 +1,34 @@ +package iavl + +import ( + "github.com/cosmos/iavl" + + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" +) + +// Importer is a wrapper around iavl.Importer. +type Importer struct { + importer *iavl.Importer +} + +// Add adds the given item to the importer. +func (i *Importer) Add(item *snapshotstypes.SnapshotIAVLItem) error { + return i.importer.Add(&iavl.ExportNode{ + Key: item.Key, + Value: item.Value, + Version: item.Version, + Height: int8(item.Height), + }) +} + +// Commit commits the importer. +func (i *Importer) Commit() error { + return i.importer.Commit() +} + +// Close closes the importer. +func (i *Importer) Close() error { + i.importer.Close() + + return nil +} diff --git a/store/commitment/iavl/tree.go b/store/commitment/iavl/tree.go index 5388769dba74..419240b5b365 100644 --- a/store/commitment/iavl/tree.go +++ b/store/commitment/iavl/tree.go @@ -77,6 +77,34 @@ func (t *IavlTree) Prune(version uint64) error { return t.tree.DeleteVersionsTo(int64(version)) } +// Export exports the tree exporter at the given version. +func (t *IavlTree) Export(version uint64) (commitment.Exporter, error) { + tree, err := t.tree.GetImmutable(int64(version)) + if err != nil { + return nil, err + } + exporter, err := tree.Export() + if err != nil { + return nil, err + } + + return &Exporter{ + exporter: exporter, + }, nil +} + +// Import imports the tree importer at the given version. +func (t *IavlTree) Import(version uint64) (commitment.Importer, error) { + importer, err := t.tree.Import(int64(version)) + if err != nil { + return nil, err + } + + return &Importer{ + importer: importer, + }, nil +} + // Close closes the iavl tree. func (t *IavlTree) Close() error { return nil diff --git a/store/commitment/iavl/tree_test.go b/store/commitment/iavl/tree_test.go index 9b73c0b7b560..a1ea79bcc7c8 100644 --- a/store/commitment/iavl/tree_test.go +++ b/store/commitment/iavl/tree_test.go @@ -5,11 +5,29 @@ import ( dbm "github.com/cosmos/cosmos-db" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" "cosmossdk.io/log" + "cosmossdk.io/store/v2/commitment" ) -func generateTree(treeType string) *IavlTree { +func TestCommitterSuite(t *testing.T) { + s := &commitment.CommitStoreTestSuite{ + NewStore: func(db dbm.DB, storeKeys []string, logger log.Logger) (*commitment.CommitStore, error) { + multiTrees := make(map[string]commitment.Tree) + cfg := DefaultConfig() + for _, storeKey := range storeKeys { + prefixDB := dbm.NewPrefixDB(db, []byte(storeKey)) + multiTrees[storeKey] = NewIavlTree(prefixDB, logger, cfg) + } + return commitment.NewCommitStore(multiTrees, logger) + }, + } + + suite.Run(t, s) +} + +func generateTree() *IavlTree { cfg := DefaultConfig() db := dbm.NewMemDB() return NewIavlTree(db, log.NewNopLogger(), cfg) @@ -17,7 +35,7 @@ func generateTree(treeType string) *IavlTree { func TestIavlTree(t *testing.T) { // generate a new tree - tree := generateTree("iavl") + tree := generateTree() require.NotNil(t, tree) initVersion := tree.GetLatestVersion() diff --git a/store/commitment/store.go b/store/commitment/store.go index 32952ef58f89..ad391b1df3a2 100644 --- a/store/commitment/store.go +++ b/store/commitment/store.go @@ -3,14 +3,22 @@ package commitment import ( "errors" "fmt" + "io" + "math" + protoio "github.com/cosmos/gogoproto/io" ics23 "github.com/cosmos/ics23/go" "cosmossdk.io/log" "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/snapshots" + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" ) -var _ store.Committer = (*CommitStore)(nil) +var ( + _ store.Committer = (*CommitStore)(nil) + _ snapshots.CommitSnapshotter = (*CommitStore)(nil) +) // CommitStore is a wrapper around multiple Tree objects mapped by a unique store // key. Each store key reflects dedicated and unique usage within a module. A caller @@ -127,6 +135,146 @@ func (c *CommitStore) Prune(version uint64) (ferr error) { return ferr } +// Snapshot implements snapshotstypes.CommitSnapshotter. +func (c *CommitStore) Snapshot(version uint64, protoWriter protoio.Writer) error { + if version == 0 { + return fmt.Errorf("the snapshot version must be greater than 0") + } + + latestVersion, err := c.GetLatestVersion() + if err != nil { + return err + } + if version > latestVersion { + return fmt.Errorf("the snapshot version %d is greater than the latest version %d", version, latestVersion) + } + + for storeKey, tree := range c.multiTrees { + // TODO: check the parallelism of this loop + if err := func() error { + exporter, err := tree.Export(version) + if err != nil { + return fmt.Errorf("failed to export tree for version %d: %w", version, err) + } + defer exporter.Close() + + err = protoWriter.WriteMsg(&snapshotstypes.SnapshotItem{ + Item: &snapshotstypes.SnapshotItem_Store{ + Store: &snapshotstypes.SnapshotStoreItem{ + Name: storeKey, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to write store name: %w", err) + } + + for { + item, err := exporter.Next() + if errors.Is(err, ErrorExportDone) { + break + } else if err != nil { + return fmt.Errorf("failed to get the next export node: %w", err) + } + + if err = protoWriter.WriteMsg(&snapshotstypes.SnapshotItem{ + Item: &snapshotstypes.SnapshotItem_IAVL{ + IAVL: item, + }, + }); err != nil { + return fmt.Errorf("failed to write iavl node: %w", err) + } + } + + return nil + }(); err != nil { + return err + } + } + + return nil +} + +// Restore implements snapshotstypes.CommitSnapshotter. +func (c *CommitStore) Restore(version uint64, format uint32, protoReader protoio.Reader, chStorage chan<- *store.KVPair) (snapshotstypes.SnapshotItem, error) { + var ( + importer Importer + snapshotItem snapshotstypes.SnapshotItem + storeKey string + ) + +loop: + for { + snapshotItem = snapshotstypes.SnapshotItem{} + err := protoReader.ReadMsg(&snapshotItem) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("invalid protobuf message: %w", err) + } + + switch item := snapshotItem.Item.(type) { + case *snapshotstypes.SnapshotItem_Store: + if importer != nil { + if err := importer.Commit(); err != nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("failed to commit importer: %w", err) + } + importer.Close() + } + storeKey = item.Store.Name + tree := c.multiTrees[storeKey] + if tree == nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("store %s not found", storeKey) + } + importer, err = tree.Import(version) + if err != nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("failed to import tree for version %d: %w", version, err) + } + defer importer.Close() + + case *snapshotstypes.SnapshotItem_IAVL: + if importer == nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("received IAVL node item before store item") + } + node := item.IAVL + if node.Height > int32(math.MaxInt8) { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("node height %v cannot exceed %v", + item.IAVL.Height, math.MaxInt8) + } + // Protobuf does not differentiate between []byte{} and nil, but fortunately IAVL does + // not allow nil keys nor nil values for leaf nodes, so we can always set them to empty. + if node.Key == nil { + node.Key = []byte{} + } + if node.Height == 0 { + if node.Value == nil { + node.Value = []byte{} + } + // If the node is a leaf node, it will be written to the storage. + chStorage <- &store.KVPair{ + Key: node.Key, + Value: node.Value, + StoreKey: storeKey, + } + } + err := importer.Add(node) + if err != nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("failed to add node to importer: %w", err) + } + default: + break loop + } + } + + if importer != nil { + if err := importer.Commit(); err != nil { + return snapshotstypes.SnapshotItem{}, fmt.Errorf("failed to commit importer: %w", err) + } + } + + return snapshotItem, c.LoadVersion(version) +} + func (c *CommitStore) Close() (ferr error) { for _, tree := range c.multiTrees { if err := tree.Close(); err != nil { diff --git a/store/commitment/store_test_suite.go b/store/commitment/store_test_suite.go new file mode 100644 index 000000000000..370958cb3f10 --- /dev/null +++ b/store/commitment/store_test_suite.go @@ -0,0 +1,121 @@ +package commitment + +import ( + "fmt" + "io" + "sync" + + dbm "github.com/cosmos/cosmos-db" + "github.com/stretchr/testify/suite" + + "cosmossdk.io/log" + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/snapshots" + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" +) + +const ( + storeKey1 = "store1" + storeKey2 = "store2" +) + +// CommitStoreTestSuite is a test suite to be used for all tree backends. +type CommitStoreTestSuite struct { + suite.Suite + + NewStore func(db dbm.DB, storeKeys []string, logger log.Logger) (*CommitStore, error) +} + +func (s *CommitStoreTestSuite) TestSnapshotter() { + storeKeys := []string{storeKey1, storeKey2} + commitStore, err := s.NewStore(dbm.NewMemDB(), storeKeys, log.NewNopLogger()) + s.Require().NoError(err) + + latestVersion := uint64(10) + kvCount := 10 + for i := uint64(1); i <= latestVersion; i++ { + kvPairs := make(map[string]store.KVPairs) + for _, storeKey := range storeKeys { + kvPairs[storeKey] = store.KVPairs{} + for j := 0; j < kvCount; j++ { + key := []byte(fmt.Sprintf("key-%d-%d", i, j)) + value := []byte(fmt.Sprintf("value-%d-%d", i, j)) + kvPairs[storeKey] = append(kvPairs[storeKey], store.KVPair{Key: key, Value: value}) + } + } + s.Require().NoError(commitStore.WriteBatch(store.NewChangeset(kvPairs))) + + _, err = commitStore.Commit() + s.Require().NoError(err) + } + + latestStoreInfos := commitStore.WorkingStoreInfos(latestVersion) + s.Require().Equal(len(storeKeys), len(latestStoreInfos)) + + // create a snapshot + dummyExtensionItem := snapshotstypes.SnapshotItem{ + Item: &snapshotstypes.SnapshotItem_Extension{ + Extension: &snapshotstypes.SnapshotExtensionMeta{ + Name: "test", + Format: 1, + }, + }, + } + + targetStore, err := s.NewStore(dbm.NewMemDB(), storeKeys, log.NewNopLogger()) + s.Require().NoError(err) + + chunks := make(chan io.ReadCloser, kvCount*int(latestVersion)) + go func() { + streamWriter := snapshots.NewStreamWriter(chunks) + s.Require().NotNil(streamWriter) + defer streamWriter.Close() + err := commitStore.Snapshot(latestVersion, streamWriter) + s.Require().NoError(err) + // write an extension metadata + err = streamWriter.WriteMsg(&dummyExtensionItem) + s.Require().NoError(err) + }() + + streamReader, err := snapshots.NewStreamReader(chunks) + s.Require().NoError(err) + chStorage := make(chan *store.KVPair, 100) + leaves := make(map[string]string) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + for kv := range chStorage { + leaves[fmt.Sprintf("%s_%s", kv.StoreKey, kv.Key)] = string(kv.Value) + } + wg.Done() + }() + nextItem, err := targetStore.Restore(latestVersion, snapshotstypes.CurrentFormat, streamReader, chStorage) + s.Require().NoError(err) + s.Require().Equal(*dummyExtensionItem.GetExtension(), *nextItem.GetExtension()) + + close(chStorage) + wg.Wait() + s.Require().Equal(len(storeKeys)*kvCount*int(latestVersion), len(leaves)) + for _, storeKey := range storeKeys { + for i := 1; i <= int(latestVersion); i++ { + for j := 0; j < kvCount; j++ { + key := fmt.Sprintf("%s_key-%d-%d", storeKey, i, j) + s.Require().Equal(leaves[key], fmt.Sprintf("value-%d-%d", i, j)) + } + } + } + + // check the restored tree hash + targetStoreInfos := targetStore.WorkingStoreInfos(latestVersion) + s.Require().Equal(len(storeKeys), len(targetStoreInfos)) + for _, storeInfo := range targetStoreInfos { + matched := false + for _, latestStoreInfo := range latestStoreInfos { + if storeInfo.Name == latestStoreInfo.Name { + s.Require().Equal(latestStoreInfo.GetHash(), storeInfo.GetHash()) + matched = true + } + } + s.Require().True(matched) + } +} diff --git a/store/commitment/tree.go b/store/commitment/tree.go index b55c90c5fad1..67acffdf3099 100644 --- a/store/commitment/tree.go +++ b/store/commitment/tree.go @@ -1,11 +1,17 @@ package commitment import ( + "errors" "io" ics23 "github.com/cosmos/ics23/go" + + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" ) +// ErrorExportDone is returned by Exporter.Next() when all items have been exported. +var ErrorExportDone = errors.New("export is complete") + // Tree is the interface that wraps the basic Tree methods. type Tree interface { Set(key, value []byte) error @@ -16,6 +22,23 @@ type Tree interface { Commit() ([]byte, error) GetProof(version uint64, key []byte) (*ics23.CommitmentProof, error) Prune(version uint64) error + Export(version uint64) (Exporter, error) + Import(version uint64) (Importer, error) + + io.Closer +} + +// Exporter is the interface that wraps the basic Export methods. +type Exporter interface { + Next() (*snapshotstypes.SnapshotIAVLItem, error) + + io.Closer +} + +// Importer is the interface that wraps the basic Import methods. +type Importer interface { + Add(*snapshotstypes.SnapshotIAVLItem) error + Commit() error io.Closer } diff --git a/store/gas.go b/store/gas.go index f7743486f09d..9bd142e506fa 100644 --- a/store/gas.go +++ b/store/gas.go @@ -9,7 +9,7 @@ import ( // SDK for store operations such as Get and Set calls. In addition, callers have // the ability to explicitly charge gas for costly operations such as signature // verification. -type Gas uint64 +type Gas = uint64 // Gas consumption descriptors. const ( diff --git a/store/go.mod b/store/go.mod index f27699b9fe14..b37ef6761c07 100644 --- a/store/go.mod +++ b/store/go.mod @@ -3,6 +3,7 @@ module cosmossdk.io/store/v2 go 1.21 require ( + cosmossdk.io/core v0.12.0 cosmossdk.io/errors v1.0.0 cosmossdk.io/log v1.2.1 cosmossdk.io/math v1.2.0 @@ -58,13 +59,13 @@ require ( github.com/rs/zerolog v1.31.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect golang.org/x/crypto v0.16.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/store/go.sum b/store/go.sum index 30e8b47236dc..b44acba0807d 100644 --- a/store/go.sum +++ b/store/go.sum @@ -1,3 +1,5 @@ +cosmossdk.io/core v0.12.0 h1:aFuvkG6eDv0IQC+UDjx86wxNWVAxdCFk7OABJ1Vh4RU= +cosmossdk.io/core v0.12.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= cosmossdk.io/errors v1.0.0 h1:nxF07lmlBbB8NKQhtJ+sJm6ef5uV1XkvPXG2bUntb04= cosmossdk.io/errors v1.0.0/go.mod h1:+hJZLuhdDE0pYN8HkOrVNwrIOYvUGnn6+4fjnJs/oV0= cosmossdk.io/log v1.2.1 h1:Xc1GgTCicniwmMiKwDxUjO4eLhPxoVdI9vtMW8Ti/uk= @@ -24,6 +26,9 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= @@ -63,6 +68,7 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= @@ -74,6 +80,7 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -90,6 +97,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -103,6 +111,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -114,6 +123,7 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -148,16 +158,21 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae h1:FatpGJD2jmJfhZiFDElaC0QhZUDQnxUeAwTGkfAHN3I= github.com/oasisprotocol/curve25519-voi v0.0.0-20220708102147-0a8a51822cae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -209,10 +224,12 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -235,10 +252,12 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -256,31 +275,42 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -306,6 +336,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/store/kv/branch/store_test.go b/store/kv/branch/store_test.go index 4353f280f65f..424e396e0b2e 100644 --- a/store/kv/branch/store_test.go +++ b/store/kv/branch/store_test.go @@ -8,6 +8,7 @@ import ( "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/kv/branch" + "cosmossdk.io/store/v2/storage" "cosmossdk.io/store/v2/storage/sqlite" ) @@ -25,7 +26,8 @@ func TestStorageTestSuite(t *testing.T) { } func (s *StoreTestSuite) SetupTest() { - storage, err := sqlite.New(s.T().TempDir()) + sqliteDB, err := sqlite.New(s.T().TempDir()) + ss := storage.NewStorageStore(sqliteDB) s.Require().NoError(err) cs := store.NewChangeset(map[string]store.KVPairs{storeKey: {}}) @@ -36,12 +38,12 @@ func (s *StoreTestSuite) SetupTest() { cs.AddKVPair(storeKey, store.KVPair{Key: []byte(key), Value: []byte(val)}) } - s.Require().NoError(storage.ApplyChangeset(1, cs)) + s.Require().NoError(ss.ApplyChangeset(1, cs)) - kvStore, err := branch.New(storeKey, storage) + kvStore, err := branch.New(storeKey, ss) s.Require().NoError(err) - s.storage = storage + s.storage = ss s.kvStore = kvStore } diff --git a/store/proof.go b/store/proof.go new file mode 100644 index 000000000000..16a959ac9075 --- /dev/null +++ b/store/proof.go @@ -0,0 +1,98 @@ +package store + +import ( + ics23 "github.com/cosmos/ics23/go" + + errorsmod "cosmossdk.io/errors" +) + +// Proof operation types +const ( + ProofOpIAVLCommitment = "ics23:iavl" + ProofOpSimpleMerkleCommitment = "ics23:simple" + ProofOpSMTCommitment = "ics23:smt" +) + +// CommitmentOp implements merkle.ProofOperator by wrapping an ics23 CommitmentProof. +// It also contains a Key field to determine which key the proof is proving. +// NOTE: CommitmentProof currently can either be ExistenceProof or NonexistenceProof +// +// Type and Spec are classified by the kind of merkle proof it represents allowing +// the code to be reused by more types. Spec is never on the wire, but mapped +// from type in the code. +type CommitmentOp struct { + Type string + Key []byte + Spec *ics23.ProofSpec + Proof *ics23.CommitmentProof +} + +func NewIAVLCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp { + return CommitmentOp{ + Type: ProofOpIAVLCommitment, + Spec: ics23.IavlSpec, + Key: key, + Proof: proof, + } +} + +func NewSimpleMerkleCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp { + return CommitmentOp{ + Type: ProofOpSimpleMerkleCommitment, + Spec: ics23.TendermintSpec, + Key: key, + Proof: proof, + } +} + +func NewSMTCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp { + return CommitmentOp{ + Type: ProofOpSMTCommitment, + Spec: ics23.SmtSpec, + Key: key, + Proof: proof, + } +} + +func (op CommitmentOp) GetKey() []byte { + return op.Key +} + +// Run takes in a list of arguments and attempts to run the proof op against these +// arguments. Returns the root wrapped in [][]byte if the proof op succeeds with +// given args. If not, it will return an error. +// +// CommitmentOp will accept args of length 1 or length 0. If length 1 args is +// passed in, then CommitmentOp will attempt to prove the existence of the key +// with the value provided by args[0] using the embedded CommitmentProof and returns +// the CommitmentRoot of the proof. If length 0 args is passed in, then CommitmentOp +// will attempt to prove the absence of the key in the CommitmentOp and return the +// CommitmentRoot of the proof. +func (op CommitmentOp) Run(args [][]byte) ([][]byte, error) { + // calculate root from proof + root, err := op.Proof.Calculate() + if err != nil { + return nil, errorsmod.Wrapf(ErrInvalidProof, "could not calculate root for proof: %v", err) + } + + // Only support an existence proof or nonexistence proof (batch proofs currently unsupported) + switch len(args) { + case 0: + // Args are nil, so we verify the absence of the key. + absent := ics23.VerifyNonMembership(op.Spec, root, op.Proof, op.Key) + if !absent { + return nil, errorsmod.Wrapf(ErrInvalidProof, "proof did not verify absence of key: %s", string(op.Key)) + } + + case 1: + // Args is length 1, verify existence of key with value args[0] + if !ics23.VerifyMembership(op.Spec, root, op.Proof, op.Key, args[0]) { + return nil, errorsmod.Wrapf(ErrInvalidProof, "proof did not verify existence of key %s with given value %x", op.Key, args[0]) + } + + default: + return nil, errorsmod.Wrapf(ErrInvalidProof, "args must be length 0 or 1, got: %d", len(args)) + } + + return [][]byte{root}, nil +} diff --git a/store/pruning/manager.go b/store/pruning/manager.go index 3440af2ea1c1..313216e4f9d6 100644 --- a/store/pruning/manager.go +++ b/store/pruning/manager.go @@ -40,11 +40,17 @@ func NewManager( // SetStorageOptions sets the state storage options. func (m *Manager) SetStorageOptions(opts Options) { + m.mtx.Lock() + defer m.mtx.Unlock() + m.storageOpts = opts } // SetCommitmentOptions sets the state commitment options. func (m *Manager) SetCommitmentOptions(opts Options) { + m.mtx.Lock() + defer m.mtx.Unlock() + m.commitmentOpts = opts } @@ -113,6 +119,7 @@ func (m *Manager) Prune(height uint64) { m.chStorage <- struct{}{} }() } + default: m.logger.Debug("storage pruning is still running; skipping", "version", pruneHeight) } @@ -134,7 +141,9 @@ func (m *Manager) Prune(height uint64) { m.chCommitment <- struct{}{} }() } + default: + m.logger.Debug("commitment pruning is still running; skipping", "version", pruneHeight) } } } diff --git a/store/pruning/manager_test.go b/store/pruning/manager_test.go index 47cfd3025438..65e7fe1b09ec 100644 --- a/store/pruning/manager_test.go +++ b/store/pruning/manager_test.go @@ -11,6 +11,7 @@ import ( "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/commitment" "cosmossdk.io/store/v2/commitment/iavl" + "cosmossdk.io/store/v2/storage" "cosmossdk.io/store/v2/storage/sqlite" ) @@ -34,8 +35,9 @@ func (s *PruningTestSuite) SetupTest() { logger = log.NewTestLogger(s.T()) } - ss, err := sqlite.New(s.T().TempDir()) + sqliteDB, err := sqlite.New(s.T().TempDir()) s.Require().NoError(err) + ss := storage.NewStorageStore(sqliteDB) tree := iavl.NewIavlTree(dbm.NewMemDB(), log.NewNopLogger(), iavl.DefaultConfig()) sc, err := commitment.NewCommitStore(map[string]commitment.Tree{"default": tree}, logger) @@ -58,7 +60,7 @@ func (s *PruningTestSuite) TestPruning() { latestVersion := uint64(100) - // write 10 batches + // write batches for i := uint64(0); i < latestVersion; i++ { version := i + 1 diff --git a/store/root/store.go b/store/root/store.go index 4ff0cbe5c060..453b81b0a0a9 100644 --- a/store/root/store.go +++ b/store/root/store.go @@ -9,6 +9,7 @@ import ( "github.com/cockroachdb/errors" + coreheader "cosmossdk.io/core/header" "cosmossdk.io/log" "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/kv/branch" @@ -43,7 +44,7 @@ type Store struct { rootKVStore store.BranchedKVStore // commitHeader reflects the header used when committing state (note, this isn't required and only used for query purposes) - commitHeader store.CommitHeader + commitHeader *coreheader.Info // lastCommitInfo reflects the last version/hash that has been committed lastCommitInfo *store.CommitInfo @@ -66,9 +67,9 @@ type Store struct { func New( logger log.Logger, - initVersion uint64, ss store.VersionedDatabase, sc store.Committer, + ssOpts, scOpts pruning.Options, m metrics.StoreMetrics, ) (store.RootStore, error) { rootKVStore, err := branch.New(defaultStoreKey, ss) @@ -77,10 +78,13 @@ func New( } pruningManager := pruning.NewManager(logger, ss, sc) + pruningManager.SetStorageOptions(ssOpts) + pruningManager.SetCommitmentOptions(scOpts) + pruningManager.Start() return &Store{ logger: logger.With("module", "root_store"), - initialVersion: initVersion, + initialVersion: 1, stateStore: ss, stateCommitment: sc, rootKVStore: rootKVStore, @@ -105,23 +109,22 @@ func (s *Store) Close() (err error) { return err } -// SetPruningOptions sets the pruning options on the SS and SC backends. -// NOTE: It will also start the pruning manager. -func (s *Store) SetPruningOptions(ssOpts, scOpts pruning.Options) { - s.pruningManager.SetStorageOptions(ssOpts) - s.pruningManager.SetCommitmentOptions(scOpts) - - s.pruningManager.Start() +func (s *Store) SetMetrics(m metrics.Metrics) { + s.telemetry = m } -// MountSCStore performs a no-op as a SC backend must be provided at initialization. -func (s *Store) MountSCStore(_ string, _ store.Committer) error { - return errors.New("cannot mount SC store; SC must be provided on initialization") +func (s *Store) SetInitialVersion(v uint64) error { + s.initialVersion = v + + // TODO(bez): Call SetInitialVersion on s.stateCommitment. + // + // Ref: https://github.com/cosmos/cosmos-sdk/issues/18597 + + return nil } -// GetSCStore returns the store's state commitment (SC) backend. Note, the store -// key is ignored as there exists only a single SC tree. -func (s *Store) GetSCStore(_ string) store.Committer { +// GetSCStore returns the store's state commitment (SC) backend. +func (s *Store) GetSCStore() store.Committer { return s.stateCommitment } @@ -191,7 +194,7 @@ func (s *Store) Query(storeKey string, version uint64, key []byte, prove bool) ( return store.QueryResult{}, err } - result.Proof = proof + result.Proof = store.NewIAVLCommitmentOp(key, proof) } return result, nil @@ -274,7 +277,7 @@ func (s *Store) TracingEnabled() bool { return s.traceWriter != nil } -func (s *Store) SetCommitHeader(h store.CommitHeader) { +func (s *Store) SetCommitHeader(h *coreheader.Info) { s.commitHeader = h } @@ -344,8 +347,8 @@ func (s *Store) Commit() ([]byte, error) { version := s.lastCommitInfo.Version - if s.commitHeader != nil && s.commitHeader.GetHeight() != version { - s.logger.Debug("commit header and version mismatch", "header_height", s.commitHeader.GetHeight(), "version", version) + if s.commitHeader != nil && uint64(s.commitHeader.Height) != version { + s.logger.Debug("commit header and version mismatch", "header_height", s.commitHeader.Height, "version", version) } changeset := s.rootKVStore.GetChangeset() @@ -361,7 +364,7 @@ func (s *Store) Commit() ([]byte, error) { } if s.commitHeader != nil { - s.lastCommitInfo.Timestamp = s.commitHeader.GetTime() + s.lastCommitInfo.Timestamp = s.commitHeader.Time } if err := s.rootKVStore.Reset(version); err != nil { diff --git a/store/root/store_test.go b/store/root/store_test.go index 5d610b64c0ff..ad020fe87193 100644 --- a/store/root/store_test.go +++ b/store/root/store_test.go @@ -12,6 +12,8 @@ import ( "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/commitment" "cosmossdk.io/store/v2/commitment/iavl" + "cosmossdk.io/store/v2/storage" + "cosmossdk.io/store/v2/pruning" "cosmossdk.io/store/v2/storage/sqlite" ) @@ -28,14 +30,15 @@ func TestStorageTestSuite(t *testing.T) { func (s *RootStoreTestSuite) SetupTest() { noopLog := log.NewNopLogger() - ss, err := sqlite.New(s.T().TempDir()) + sqliteDB, err := sqlite.New(s.T().TempDir()) s.Require().NoError(err) + ss := storage.NewStorageStore(sqliteDB) tree := iavl.NewIavlTree(dbm.NewMemDB(), noopLog, iavl.DefaultConfig()) - sc, err := commitment.NewCommitStore(map[string]commitment.Tree{"default": tree}, noopLog) + sc, err := commitment.NewCommitStore(map[string]commitment.Tree{defaultStoreKey: tree}, noopLog) s.Require().NoError(err) - rs, err := New(noopLog, 1, ss, sc, nil) + rs, err := New(noopLog, ss, sc, pruning.DefaultOptions(), pruning.DefaultOptions(), nil) s.Require().NoError(err) rs.SetTracer(io.Discard) @@ -51,12 +54,8 @@ func (s *RootStoreTestSuite) TearDownTest() { s.Require().NoError(err) } -func (s *RootStoreTestSuite) TestMountSCStore() { - s.Require().Error(s.rootStore.MountSCStore("", nil)) -} - func (s *RootStoreTestSuite) TestGetSCStore() { - s.Require().Equal(s.rootStore.GetSCStore(""), s.rootStore.(*Store).stateCommitment) + s.Require().Equal(s.rootStore.GetSCStore(), s.rootStore.(*Store).stateCommitment) } func (s *RootStoreTestSuite) TestGetKVStore() { @@ -90,9 +89,9 @@ func (s *RootStoreTestSuite) TestQuery() { // ensure the proof is non-nil for the corresponding version result, err := s.rootStore.Query(defaultStoreKey, 1, []byte("foo"), true) s.Require().NoError(err) - s.Require().NotNil(result.Proof) - s.Require().Equal([]byte("foo"), result.Proof.GetExist().Key) - s.Require().Equal([]byte("bar"), result.Proof.GetExist().Value) + s.Require().NotNil(result.Proof.Proof) + s.Require().Equal([]byte("foo"), result.Proof.Proof.GetExist().Key) + s.Require().Equal([]byte("bar"), result.Proof.Proof.GetExist().Value) } func (s *RootStoreTestSuite) TestBranch() { diff --git a/store/snapshots/chunk.go b/store/snapshots/chunk.go index c70fc074b0e0..874bf966871b 100644 --- a/store/snapshots/chunk.go +++ b/store/snapshots/chunk.go @@ -6,7 +6,7 @@ import ( "cosmossdk.io/errors" "cosmossdk.io/store/v2" - snapshottypes "cosmossdk.io/store/v2/snapshots/types" + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" ) // ChunkWriter reads an input stream, splits it into fixed-size chunks, and writes them to a @@ -169,15 +169,15 @@ func DrainChunks(chunks <-chan io.ReadCloser) { // ValidRestoreHeight will check height is valid for snapshot restore or not func ValidRestoreHeight(format uint32, height uint64) error { - if format != snapshottypes.CurrentFormat { - return errors.Wrapf(snapshottypes.ErrUnknownFormat, "format %v", format) + if format != snapshotstypes.CurrentFormat { + return errors.Wrapf(snapshotstypes.ErrUnknownFormat, "format %v", format) } if height == 0 { return errors.Wrap(store.ErrLogic, "cannot restore snapshot at height 0") } if height > uint64(math.MaxInt64) { - return errors.Wrapf(snapshottypes.ErrInvalidMetadata, + return errors.Wrapf(snapshotstypes.ErrInvalidMetadata, "snapshot height %v cannot exceed %v", height, int64(math.MaxInt64)) } diff --git a/store/snapshots/helpers_test.go b/store/snapshots/helpers_test.go index 7c6cf04bcd72..9711ab538347 100644 --- a/store/snapshots/helpers_test.go +++ b/store/snapshots/helpers_test.go @@ -17,8 +17,9 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" + "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/snapshots" - snapshottypes "cosmossdk.io/store/v2/snapshots/types" + snapshotstypes "cosmossdk.io/store/v2/snapshots/types" ) func checksums(slice [][]byte) [][]byte { @@ -62,7 +63,7 @@ func readChunks(chunks <-chan io.ReadCloser) [][]byte { } // snapshotItems serialize a array of bytes as SnapshotItem_ExtensionPayload, and return the chunks. -func snapshotItems(items [][]byte, ext snapshottypes.ExtensionSnapshotter) [][]byte { +func snapshotItems(items [][]byte, ext snapshots.ExtensionSnapshotter) [][]byte { // copy the same parameters from the code snapshotChunkSize := uint64(10e6) snapshotBufferSize := int(snapshotChunkSize) @@ -74,19 +75,19 @@ func snapshotItems(items [][]byte, ext snapshottypes.ExtensionSnapshotter) [][]b zWriter, _ := zlib.NewWriterLevel(bufWriter, 7) protoWriter := protoio.NewDelimitedWriter(zWriter) for _, item := range items { - _ = snapshottypes.WriteExtensionPayload(protoWriter, item) + _ = snapshotstypes.WriteExtensionPayload(protoWriter, item) } // write extension metadata - _ = protoWriter.WriteMsg(&snapshottypes.SnapshotItem{ - Item: &snapshottypes.SnapshotItem_Extension{ - Extension: &snapshottypes.SnapshotExtensionMeta{ + _ = protoWriter.WriteMsg(&snapshotstypes.SnapshotItem{ + Item: &snapshotstypes.SnapshotItem_Extension{ + Extension: &snapshotstypes.SnapshotExtensionMeta{ Name: ext.SnapshotName(), Format: ext.SnapshotFormat(), }, }, }) _ = ext.SnapshotExtension(0, func(payload []byte) error { - return snapshottypes.WriteExtensionPayload(protoWriter, payload) + return snapshotstypes.WriteExtensionPayload(protoWriter, payload) }) _ = protoWriter.Close() _ = bufWriter.Flush() @@ -105,23 +106,21 @@ func snapshotItems(items [][]byte, ext snapshottypes.ExtensionSnapshotter) [][]b return chunks } -type mockSnapshotter struct { - items [][]byte - prunedHeights map[int64]struct{} - snapshotInterval uint64 +type mockCommitSnapshotter struct { + items [][]byte } -func (m *mockSnapshotter) Restore( - height uint64, format uint32, protoReader protoio.Reader, -) (snapshottypes.SnapshotItem, error) { +func (m *mockCommitSnapshotter) Restore( + height uint64, format uint32, protoReader protoio.Reader, chStorage chan<- *store.KVPair, +) (snapshotstypes.SnapshotItem, error) { if format == 0 { - return snapshottypes.SnapshotItem{}, snapshottypes.ErrUnknownFormat + return snapshotstypes.SnapshotItem{}, snapshotstypes.ErrUnknownFormat } if m.items != nil { - return snapshottypes.SnapshotItem{}, errors.New("already has contents") + return snapshotstypes.SnapshotItem{}, errors.New("already has contents") } - var item snapshottypes.SnapshotItem + var item snapshotstypes.SnapshotItem m.items = [][]byte{} for { item.Reset() @@ -129,7 +128,7 @@ func (m *mockSnapshotter) Restore( if err == io.EOF { break } else if err != nil { - return snapshottypes.SnapshotItem{}, errorsmod.Wrap(err, "invalid protobuf message") + return snapshotstypes.SnapshotItem{}, errorsmod.Wrap(err, "invalid protobuf message") } payload := item.GetExtensionPayload() if payload == nil { @@ -141,65 +140,49 @@ func (m *mockSnapshotter) Restore( return item, nil } -func (m *mockSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { +func (m *mockCommitSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { for _, item := range m.items { - if err := snapshottypes.WriteExtensionPayload(protoWriter, item); err != nil { + if err := snapshotstypes.WriteExtensionPayload(protoWriter, item); err != nil { return err } } return nil } -func (m *mockSnapshotter) SnapshotFormat() uint32 { - return snapshottypes.CurrentFormat +func (m *mockCommitSnapshotter) SnapshotFormat() uint32 { + return snapshotstypes.CurrentFormat } -func (m *mockSnapshotter) SupportedFormats() []uint32 { - return []uint32{snapshottypes.CurrentFormat} +func (m *mockCommitSnapshotter) SupportedFormats() []uint32 { + return []uint32{snapshotstypes.CurrentFormat} } -func (m *mockSnapshotter) PruneSnapshotHeight(height int64) { - m.prunedHeights[height] = struct{}{} -} - -func (m *mockSnapshotter) GetSnapshotInterval() uint64 { - return m.snapshotInterval -} +type mockStorageSnapshotter struct{} -func (m *mockSnapshotter) SetSnapshotInterval(snapshotInterval uint64) { - m.snapshotInterval = snapshotInterval +func (m *mockStorageSnapshotter) Restore(version uint64, chStorage <-chan *store.KVPair) error { + return nil } -type mockErrorSnapshotter struct{} +type mockErrorCommitSnapshotter struct{} -var _ snapshottypes.Snapshotter = (*mockErrorSnapshotter)(nil) +var _ snapshots.CommitSnapshotter = (*mockErrorCommitSnapshotter)(nil) -func (m *mockErrorSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { +func (m *mockErrorCommitSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { return errors.New("mock snapshot error") } -func (m *mockErrorSnapshotter) Restore( - height uint64, format uint32, protoReader protoio.Reader, -) (snapshottypes.SnapshotItem, error) { - return snapshottypes.SnapshotItem{}, errors.New("mock restore error") -} - -func (m *mockErrorSnapshotter) SnapshotFormat() uint32 { - return snapshottypes.CurrentFormat -} - -func (m *mockErrorSnapshotter) SupportedFormats() []uint32 { - return []uint32{snapshottypes.CurrentFormat} -} - -func (m *mockErrorSnapshotter) PruneSnapshotHeight(height int64) { +func (m *mockErrorCommitSnapshotter) Restore( + height uint64, format uint32, protoReader protoio.Reader, chStorage chan<- *store.KVPair, +) (snapshotstypes.SnapshotItem, error) { + return snapshotstypes.SnapshotItem{}, errors.New("mock restore error") } -func (m *mockErrorSnapshotter) GetSnapshotInterval() uint64 { - return 0 +func (m *mockErrorCommitSnapshotter) SnapshotFormat() uint32 { + return snapshotstypes.CurrentFormat } -func (m *mockErrorSnapshotter) SetSnapshotInterval(snapshotInterval uint64) { +func (m *mockErrorCommitSnapshotter) SupportedFormats() []uint32 { + return []uint32{snapshotstypes.CurrentFormat} } // setupBusyManager creates a manager with an empty store that is busy creating a snapshot at height 1. @@ -208,10 +191,8 @@ func setupBusyManager(t *testing.T) *snapshots.Manager { t.Helper() store, err := snapshots.NewStore(db.NewMemDB(), t.TempDir()) require.NoError(t, err) - hung := newHungSnapshotter() - hung.SetSnapshotInterval(opts.Interval) - mgr := snapshots.NewManager(store, opts, hung, nil, log.NewNopLogger()) - require.Equal(t, opts.Interval, hung.snapshotInterval) + hung := newHungCommitSnapshotter() + mgr := snapshots.NewManager(store, opts, hung, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) // Channel to ensure the test doesn't finish until the goroutine is done. // Without this, there are intermittent test failures about @@ -222,8 +203,6 @@ func setupBusyManager(t *testing.T) *snapshots.Manager { defer close(done) _, err := mgr.Create(1) require.NoError(t, err) - _, didPruneHeight := hung.prunedHeights[1] - require.True(t, didPruneHeight) }() time.Sleep(10 * time.Millisecond) @@ -236,40 +215,29 @@ func setupBusyManager(t *testing.T) *snapshots.Manager { return mgr } -// hungSnapshotter can be used to test operations in progress. Call close to end the snapshot. -type hungSnapshotter struct { - ch chan struct{} - prunedHeights map[int64]struct{} - snapshotInterval uint64 +// hungCommitSnapshotter can be used to test operations in progress. Call close to end the snapshot. +type hungCommitSnapshotter struct { + ch chan struct{} } -func newHungSnapshotter() *hungSnapshotter { - return &hungSnapshotter{ - ch: make(chan struct{}), - prunedHeights: make(map[int64]struct{}), +func newHungCommitSnapshotter() *hungCommitSnapshotter { + return &hungCommitSnapshotter{ + ch: make(chan struct{}), } } -func (m *hungSnapshotter) Close() { +func (m *hungCommitSnapshotter) Close() { close(m.ch) } -func (m *hungSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { +func (m *hungCommitSnapshotter) Snapshot(height uint64, protoWriter protoio.Writer) error { <-m.ch return nil } -func (m *hungSnapshotter) PruneSnapshotHeight(height int64) { - m.prunedHeights[height] = struct{}{} -} - -func (m *hungSnapshotter) SetSnapshotInterval(snapshotInterval uint64) { - m.snapshotInterval = snapshotInterval -} - -func (m *hungSnapshotter) Restore( - height uint64, format uint32, protoReader protoio.Reader, -) (snapshottypes.SnapshotItem, error) { +func (m *hungCommitSnapshotter) Restore( + height uint64, format uint32, protoReader protoio.Reader, chStorage chan<- *store.KVPair, +) (snapshotstypes.SnapshotItem, error) { panic("not implemented") } @@ -299,16 +267,16 @@ func (s *extSnapshotter) SupportedFormats() []uint32 { return []uint32{1} } -func (s *extSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshottypes.ExtensionPayloadWriter) error { +func (s *extSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshots.ExtensionPayloadWriter) error { for _, i := range s.state { - if err := payloadWriter(snapshottypes.Uint64ToBigEndian(i)); err != nil { + if err := payloadWriter(snapshotstypes.Uint64ToBigEndian(i)); err != nil { return err } } return nil } -func (s *extSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshottypes.ExtensionPayloadReader) error { +func (s *extSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshots.ExtensionPayloadReader) error { for { payload, err := payloadReader() if err == io.EOF { @@ -316,7 +284,7 @@ func (s *extSnapshotter) RestoreExtension(height uint64, format uint32, payloadR } else if err != nil { return err } - s.state = append(s.state, snapshottypes.BigEndianToUint64(payload)) + s.state = append(s.state, snapshotstypes.BigEndianToUint64(payload)) } // finalize restoration return nil diff --git a/store/snapshots/manager.go b/store/snapshots/manager.go index 8dd1381e1e2b..1c2d4ec65a31 100644 --- a/store/snapshots/manager.go +++ b/store/snapshots/manager.go @@ -31,13 +31,16 @@ import ( // 2. io.ReadCloser streams automatically propagate IO errors, and can pass arbitrary // errors via io.Pipe.CloseWithError(). type Manager struct { - extensions map[string]types.ExtensionSnapshotter + extensions map[string]ExtensionSnapshotter // store is the snapshot store where all completed snapshots are persisted. store *Store - opts types.SnapshotOptions - // multistore is the store from which snapshots are taken. - multistore types.Snapshotter - logger log.Logger + opts SnapshotOptions + // commitSnapshotter is the snapshotter for the commitment state. + commitSnapshotter CommitSnapshotter + // storageSnapshotter is the snapshotter for the storage state. + storageSnapshotter StorageSnapshotter + + logger log.Logger mtx sync.Mutex operation operation @@ -62,8 +65,9 @@ const ( opPrune operation = "prune" opRestore operation = "restore" - chunkBufferSize = 4 - chunkIDBufferSize = 1024 + chunkBufferSize = 4 + chunkIDBufferSize = 1024 + defaultStorageChannelBufferSize = 1024 snapshotMaxItemSize = int(64e6) // SDK has no key/value size limit, so we set an arbitrary limit ) @@ -71,23 +75,24 @@ const ( var ErrOptsZeroSnapshotInterval = errors.New("snaphot-interval must not be 0") // NewManager creates a new manager. -func NewManager(store *Store, opts types.SnapshotOptions, multistore types.Snapshotter, extensions map[string]types.ExtensionSnapshotter, logger log.Logger) *Manager { +func NewManager(store *Store, opts SnapshotOptions, commitSnapshotter CommitSnapshotter, storageSnapshotter StorageSnapshotter, extensions map[string]ExtensionSnapshotter, logger log.Logger) *Manager { if extensions == nil { - extensions = map[string]types.ExtensionSnapshotter{} + extensions = map[string]ExtensionSnapshotter{} } return &Manager{ - store: store, - opts: opts, - multistore: multistore, - extensions: extensions, - logger: logger, + store: store, + opts: opts, + commitSnapshotter: commitSnapshotter, + storageSnapshotter: storageSnapshotter, + extensions: extensions, + logger: logger.With("module", "snapshot_manager"), } } // RegisterExtensions register extension snapshotters to manager -func (m *Manager) RegisterExtensions(extensions ...types.ExtensionSnapshotter) error { +func (m *Manager) RegisterExtensions(extensions ...ExtensionSnapshotter) error { if m.extensions == nil { - m.extensions = make(map[string]types.ExtensionSnapshotter, len(extensions)) + m.extensions = make(map[string]ExtensionSnapshotter, len(extensions)) } for _, extension := range extensions { name := extension.SnapshotName() @@ -161,11 +166,9 @@ func (m *Manager) GetSnapshotBlockRetentionHeights() int64 { // Create creates a snapshot and returns its metadata. func (m *Manager) Create(height uint64) (*types.Snapshot, error) { if m == nil { - return nil, errorsmod.Wrap(store.ErrLogic, "no snapshot store configured") + return nil, errorsmod.Wrap(store.ErrLogic, "Snapshot Manager is nil") } - defer m.multistore.PruneSnapshotHeight(int64(height)) - err := m.begin(opSnapshot) if err != nil { return nil, err @@ -201,7 +204,7 @@ func (m *Manager) createSnapshot(height uint64, ch chan<- io.ReadCloser) { } }() - if err := m.multistore.Snapshot(height, streamWriter); err != nil { + if err := m.commitSnapshotter.Snapshot(height, streamWriter); err != nil { streamWriter.CloseWithError(err) return } @@ -363,7 +366,20 @@ func (m *Manager) doRestoreSnapshot(snapshot types.Snapshot, chChunks <-chan io. return payload.Payload, nil } - nextItem, err = m.multistore.Restore(snapshot.Height, snapshot.Format, streamReader) + // chStorage is the channel to pass the KV pairs to the storage snapshotter. + chStorage := make(chan *store.KVPair, defaultStorageChannelBufferSize) + defer close(chStorage) + + storageErrs := make(chan error, 1) + go func() { + defer close(storageErrs) + err := m.storageSnapshotter.Restore(snapshot.Height, chStorage) + if err != nil { + storageErrs <- err + } + }() + + nextItem, err = m.commitSnapshotter.Restore(snapshot.Height, snapshot.Format, streamReader, chStorage) if err != nil { return errorsmod.Wrap(err, "multistore restore") } @@ -393,6 +409,12 @@ func (m *Manager) doRestoreSnapshot(snapshot types.Snapshot, chChunks <-chan io. return errorsmod.Wrapf(err, "extension %s don't exhausted payload stream", metadata.Name) } } + + // wait for storage snapshotter to complete + if err := <-storageErrs; err != nil { + return errorsmod.Wrap(err, "storage snapshotter") + } + return nil } @@ -495,7 +517,7 @@ func (m *Manager) sortedExtensionNames() []string { } // IsFormatSupported returns if the snapshotter supports restoration from given format. -func IsFormatSupported(snapshotter types.ExtensionSnapshotter, format uint32) bool { +func IsFormatSupported(snapshotter ExtensionSnapshotter, format uint32) bool { for _, i := range snapshotter.SupportedFormats() { if i == format { return true diff --git a/store/snapshots/manager_test.go b/store/snapshots/manager_test.go index c3276d01ed7e..af5b6eb1e130 100644 --- a/store/snapshots/manager_test.go +++ b/store/snapshots/manager_test.go @@ -13,14 +13,13 @@ import ( "cosmossdk.io/store/v2/snapshots/types" ) -var opts = types.NewSnapshotOptions(1500, 2) +var opts = snapshots.NewSnapshotOptions(1500, 2) func TestManager_List(t *testing.T) { store := setupStore(t) - snapshotter := &mockSnapshotter{} - snapshotter.SetSnapshotInterval(opts.Interval) - manager := snapshots.NewManager(store, opts, snapshotter, nil, log.NewNopLogger()) - require.Equal(t, opts.Interval, snapshotter.GetSnapshotInterval()) + commitSnapshotter := &mockCommitSnapshotter{} + storageSnapshotter := &mockStorageSnapshotter{} + manager := snapshots.NewManager(store, opts, commitSnapshotter, storageSnapshotter, nil, log.NewNopLogger()) mgrList, err := manager.List() require.NoError(t, err) @@ -41,7 +40,7 @@ func TestManager_List(t *testing.T) { func TestManager_LoadChunk(t *testing.T) { store := setupStore(t) - manager := snapshots.NewManager(store, opts, &mockSnapshotter{}, nil, log.NewNopLogger()) + manager := snapshots.NewManager(store, opts, &mockCommitSnapshotter{}, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) // Existing chunk should return body chunk, err := manager.LoadChunk(2, 1, 1) @@ -67,14 +66,13 @@ func TestManager_Take(t *testing.T) { {4, 5, 6}, {7, 8, 9}, } - snapshotter := &mockSnapshotter{ - items: items, - prunedHeights: make(map[int64]struct{}), + commitSnapshotter := &mockCommitSnapshotter{ + items: items, } extSnapshotter := newExtSnapshotter(10) expectChunks := snapshotItems(items, extSnapshotter) - manager := snapshots.NewManager(store, opts, snapshotter, nil, log.NewNopLogger()) + manager := snapshots.NewManager(store, opts, commitSnapshotter, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) err := manager.RegisterExtensions(extSnapshotter) require.NoError(t, err) @@ -85,18 +83,14 @@ func TestManager_Take(t *testing.T) { // creating a snapshot at a lower height than the latest should error _, err = manager.Create(3) require.Error(t, err) - _, didPruneHeight := snapshotter.prunedHeights[3] - require.True(t, didPruneHeight) // creating a snapshot at a higher height should be fine, and should return it snapshot, err := manager.Create(5) require.NoError(t, err) - _, didPruneHeight = snapshotter.prunedHeights[5] - require.True(t, didPruneHeight) assert.Equal(t, &types.Snapshot{ Height: 5, - Format: snapshotter.SnapshotFormat(), + Format: commitSnapshotter.SnapshotFormat(), Chunks: 1, Hash: []uint8{0xc5, 0xf7, 0xfe, 0xea, 0xd3, 0x4d, 0x3e, 0x87, 0xff, 0x41, 0xa2, 0x27, 0xfa, 0xcb, 0x38, 0x17, 0xa, 0x5, 0xeb, 0x27, 0x4e, 0x16, 0x5e, 0xf3, 0xb2, 0x8b, 0x47, 0xd1, 0xe6, 0x94, 0x7e, 0x8b}, Metadata: types.Metadata{ @@ -117,9 +111,7 @@ func TestManager_Take(t *testing.T) { func TestManager_Prune(t *testing.T) { store := setupStore(t) - snapshotter := &mockSnapshotter{} - snapshotter.SetSnapshotInterval(opts.Interval) - manager := snapshots.NewManager(store, opts, snapshotter, nil, log.NewNopLogger()) + manager := snapshots.NewManager(store, opts, &mockCommitSnapshotter{}, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) pruned, err := manager.Prune(2) require.NoError(t, err) @@ -137,11 +129,9 @@ func TestManager_Prune(t *testing.T) { func TestManager_Restore(t *testing.T) { store := setupStore(t) - target := &mockSnapshotter{ - prunedHeights: make(map[int64]struct{}), - } + target := &mockCommitSnapshotter{} extSnapshotter := newExtSnapshotter(0) - manager := snapshots.NewManager(store, opts, target, nil, log.NewNopLogger()) + manager := snapshots.NewManager(store, opts, target, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) err := manager.RegisterExtensions(extSnapshotter) require.NoError(t, err) @@ -191,8 +181,6 @@ func TestManager_Restore(t *testing.T) { // While the restore is in progress, any other operations fail _, err = manager.Create(4) require.Error(t, err) - _, didPruneHeight := target.prunedHeights[4] - require.True(t, didPruneHeight) _, err = manager.Prune(1) require.Error(t, err) @@ -248,10 +236,10 @@ func TestManager_Restore(t *testing.T) { } func TestManager_TakeError(t *testing.T) { - snapshotter := &mockErrorSnapshotter{} + snapshotter := &mockErrorCommitSnapshotter{} store, err := snapshots.NewStore(db.NewMemDB(), GetTempDir(t)) require.NoError(t, err) - manager := snapshots.NewManager(store, opts, snapshotter, nil, log.NewNopLogger()) + manager := snapshots.NewManager(store, opts, snapshotter, &mockStorageSnapshotter{}, nil, log.NewNopLogger()) _, err = manager.Create(1) require.Error(t, err) diff --git a/store/snapshots/types/options.go b/store/snapshots/options.go similarity index 96% rename from store/snapshots/types/options.go rename to store/snapshots/options.go index 9c6ec79a11e2..565a0ce105de 100644 --- a/store/snapshots/types/options.go +++ b/store/snapshots/options.go @@ -1,4 +1,4 @@ -package types +package snapshots // SnapshotOptions defines the snapshot strategy used when determining which // heights are snapshotted for state sync. diff --git a/store/snapshots/types/snapshotter.go b/store/snapshots/snapshotter.go similarity index 55% rename from store/snapshots/types/snapshotter.go rename to store/snapshots/snapshotter.go index de9fcfe3d3ff..7c8321f3c838 100644 --- a/store/snapshots/types/snapshotter.go +++ b/store/snapshots/snapshotter.go @@ -1,29 +1,26 @@ -package types +package snapshots import ( protoio "github.com/cosmos/gogoproto/io" + + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/snapshots/types" ) -// Snapshotter is something that can create and restore snapshots, consisting of streamed binary -// chunks - all of which must be read from the channel and closed. If an unsupported format is -// given, it must return ErrUnknownFormat (possibly wrapped with fmt.Errorf). -type Snapshotter interface { - // Snapshot writes snapshot items into the protobuf writer. - Snapshot(height uint64, protoWriter protoio.Writer) error - - // PruneSnapshotHeight prunes the given height according to the prune strategy. - // If PruneNothing, this is a no-op. - // If other strategy, this height is persisted until it is - // less than - KeepRecent and % Interval == 0 - PruneSnapshotHeight(height int64) - - // SetSnapshotInterval sets the interval at which the snapshots are taken. - // It is used by the store that implements the Snapshotter interface - // to determine which heights to retain until after the snapshot is complete. - SetSnapshotInterval(snapshotInterval uint64) - - // Restore restores a state snapshot, taking the reader of protobuf message stream as input. - Restore(height uint64, format uint32, protoReader protoio.Reader) (SnapshotItem, error) +// CommitSnapshotter defines an API for creating and restoring snapshots of the +// commitment state. +type CommitSnapshotter interface { + // Snapshot writes a snapshot of the commitment state at the given version. + Snapshot(version uint64, protoWriter protoio.Writer) error + + // Restore restores the commitment state from the snapshot reader. + Restore(version uint64, format uint32, protoReader protoio.Reader, chStorage chan<- *store.KVPair) (types.SnapshotItem, error) +} + +// StorageSnapshotter defines an API for restoring snapshots of the storage state. +type StorageSnapshotter interface { + // Restore restores the storage state from the given channel. + Restore(version uint64, chStorage <-chan *store.KVPair) error } // ExtensionPayloadReader read extension payloads, diff --git a/store/storage/database.go b/store/storage/database.go new file mode 100644 index 000000000000..884981f6138f --- /dev/null +++ b/store/storage/database.go @@ -0,0 +1,25 @@ +package storage + +import ( + "io" + + "cosmossdk.io/store/v2" +) + +// Database is an interface that wraps the storage database methods. A wrapper +// is useful for instances where you want to perform logic that is identical for all SS +// backends, such as restoring snapshots. +type Database interface { + NewBatch(version uint64) (store.Batch, error) + Has(storeKey string, version uint64, key []byte) (bool, error) + Get(storeKey string, version uint64, key []byte) ([]byte, error) + GetLatestVersion() (uint64, error) + SetLatestVersion(version uint64) error + + Iterator(storeKey string, version uint64, start, end []byte) (store.Iterator, error) + ReverseIterator(storeKey string, version uint64, start, end []byte) (store.Iterator, error) + + Prune(version uint64) error + + io.Closer +} diff --git a/store/storage/pebbledb/db.go b/store/storage/pebbledb/db.go index 4e61857f9352..42b9a2812ca4 100644 --- a/store/storage/pebbledb/db.go +++ b/store/storage/pebbledb/db.go @@ -11,6 +11,7 @@ import ( "github.com/cockroachdb/pebble" "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/storage" ) const ( @@ -25,7 +26,7 @@ const ( tombstoneVal = "TOMBSTONE" ) -var _ store.VersionedDatabase = (*Database)(nil) +var _ storage.Database = (*Database)(nil) type Database struct { storage *pebble.DB @@ -92,6 +93,15 @@ func (db *Database) Close() error { return err } +func (db *Database) NewBatch(version uint64) (store.Batch, error) { + b, err := NewBatch(db.storage, version, db.sync) + if err != nil { + return nil, err + } + + return b, nil +} + func (db *Database) SetLatestVersion(version uint64) error { var ts [VersionSize]byte binary.LittleEndian.PutUint64(ts[:], version) @@ -175,29 +185,6 @@ func (db *Database) Get(storeKey string, targetVersion uint64, key []byte) ([]by return nil, nil } -func (db *Database) ApplyChangeset(version uint64, cs *store.Changeset) error { - b, err := NewBatch(db.storage, version, db.sync) - if err != nil { - return err - } - - for storeKey, pairs := range cs.Pairs { - for _, kvPair := range pairs { - if kvPair.Value == nil { - if err := b.Delete(storeKey, kvPair.Key); err != nil { - return err - } - } else { - if err := b.Set(storeKey, kvPair.Key, kvPair.Value); err != nil { - return err - } - } - } - } - - return b.Write() -} - // Prune removes all versions of all keys that are <= the given version. // // Note, the implementation of this method is inefficient and can be potentially diff --git a/store/storage/pebbledb/db_test.go b/store/storage/pebbledb/db_test.go index 934660042167..a8310386bc37 100644 --- a/store/storage/pebbledb/db_test.go +++ b/store/storage/pebbledb/db_test.go @@ -19,7 +19,7 @@ func TestStorageTestSuite(t *testing.T) { db.SetSync(false) } - return db, err + return storage.NewStorageStore(db), err }, EmptyBatchSize: 12, } diff --git a/store/storage/rocksdb/db.go b/store/storage/rocksdb/db.go index d73fd29be5ee..d1806cce1e2d 100644 --- a/store/storage/rocksdb/db.go +++ b/store/storage/rocksdb/db.go @@ -12,6 +12,7 @@ import ( "golang.org/x/exp/slices" "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/storage" "cosmossdk.io/store/v2/storage/util" ) @@ -23,7 +24,7 @@ const ( ) var ( - _ store.VersionedDatabase = (*Database)(nil) + _ storage.Database = (*Database)(nil) defaultWriteOpts = grocksdb.NewDefaultWriteOptions() defaultReadOpts = grocksdb.NewDefaultReadOptions() @@ -90,6 +91,10 @@ func (db *Database) Close() error { return nil } +func (db *Database) NewBatch(version uint64) (store.Batch, error) { + return NewBatch(db, version), nil +} + func (db *Database) getSlice(storeKey string, version uint64, key []byte) (*grocksdb.Slice, error) { if version < db.tsLow { return nil, store.ErrVersionPruned{EarliestVersion: db.tsLow} @@ -141,26 +146,6 @@ func (db *Database) Get(storeKey string, version uint64, key []byte) ([]byte, er return copyAndFreeSlice(slice), nil } -func (db *Database) ApplyChangeset(version uint64, cs *store.Changeset) error { - b := NewBatch(db, version) - - for storeKey, pairs := range cs.Pairs { - for _, kvPair := range pairs { - if kvPair.Value == nil { - if err := b.Delete(storeKey, kvPair.Key); err != nil { - return err - } - } else { - if err := b.Set(storeKey, kvPair.Key, kvPair.Value); err != nil { - return err - } - } - } - } - - return b.Write() -} - // Prune attempts to prune all versions up to and including the provided version. // This is done internally by updating the full_history_ts_low RocksDB value on // the column families, s.t. all versions less than full_history_ts_low will be diff --git a/store/storage/rocksdb/db_test.go b/store/storage/rocksdb/db_test.go index c1d2868cc68f..8788bfe632e2 100644 --- a/store/storage/rocksdb/db_test.go +++ b/store/storage/rocksdb/db_test.go @@ -21,7 +21,8 @@ const ( func TestStorageTestSuite(t *testing.T) { s := &storage.StorageTestSuite{ NewDB: func(dir string) (store.VersionedDatabase, error) { - return New(dir) + db, err := New(dir) + return storage.NewStorageStore(db), err }, EmptyBatchSize: 12, } @@ -33,15 +34,15 @@ func TestDatabase_ReverseIterator(t *testing.T) { require.NoError(t, err) defer db.Close() - cs := store.NewChangeset(map[string]store.KVPairs{storeKey1: {}}) + batch := NewBatch(db, 1) for i := 0; i < 100; i++ { key := fmt.Sprintf("key%03d", i) // key000, key001, ..., key099 val := fmt.Sprintf("val%03d", i) // val000, val001, ..., val099 - cs.AddKVPair(storeKey1, store.KVPair{Key: []byte(key), Value: []byte(val)}) + require.NoError(t, batch.Set(storeKey1, []byte(key), []byte(val))) } - require.NoError(t, db.ApplyChangeset(1, cs)) + require.NoError(t, batch.Write()) // reverse iterator without an end key iter, err := db.ReverseIterator(storeKey1, 1, []byte("key000"), nil) diff --git a/store/storage/sqlite/db.go b/store/storage/sqlite/db.go index 9a1c3421b806..05fde45b03fd 100644 --- a/store/storage/sqlite/db.go +++ b/store/storage/sqlite/db.go @@ -11,6 +11,7 @@ import ( _ "github.com/mattn/go-sqlite3" "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/storage" ) const ( @@ -40,7 +41,7 @@ const ( ` ) -var _ store.VersionedDatabase = (*Database)(nil) +var _ storage.Database = (*Database)(nil) type Database struct { storage *sql.DB @@ -91,6 +92,10 @@ func (db *Database) Close() error { return err } +func (db *Database) NewBatch(version uint64) (store.Batch, error) { + return NewBatch(db.storage, version) +} + func (db *Database) GetLatestVersion() (uint64, error) { stmt, err := db.storage.Prepare("SELECT value FROM state_storage WHERE store_key = ? AND key = ?") if err != nil { @@ -168,29 +173,6 @@ func (db *Database) Get(storeKey string, targetVersion uint64, key []byte) ([]by return nil, nil } -func (db *Database) ApplyChangeset(version uint64, cs *store.Changeset) error { - b, err := NewBatch(db.storage, version) - if err != nil { - return err - } - - for storeKey, pairs := range cs.Pairs { - for _, kvPair := range pairs { - if kvPair.Value == nil { - if err := b.Delete(storeKey, kvPair.Key); err != nil { - return err - } - } else { - if err := b.Set(storeKey, kvPair.Key, kvPair.Value); err != nil { - return err - } - } - } - } - - return b.Write() -} - // Prune removes all versions of all keys that are <= the given version. It keeps // the latest (non-tombstoned) version of each key/value tuple to handle queries // above the prune version. This is analogous to RocksDB full_history_ts_low. diff --git a/store/storage/sqlite/db_test.go b/store/storage/sqlite/db_test.go index f80f88baf7e0..deff98ec4348 100644 --- a/store/storage/sqlite/db_test.go +++ b/store/storage/sqlite/db_test.go @@ -19,7 +19,8 @@ const ( func TestStorageTestSuite(t *testing.T) { s := &storage.StorageTestSuite{ NewDB: func(dir string) (store.VersionedDatabase, error) { - return New(dir) + db, err := New(dir) + return storage.NewStorageStore(db), err }, EmptyBatchSize: 0, } @@ -31,15 +32,16 @@ func TestDatabase_ReverseIterator(t *testing.T) { require.NoError(t, err) defer db.Close() - cs := store.NewChangeset(map[string]store.KVPairs{storeKey1: {}}) + batch, err := db.NewBatch(1) + require.NoError(t, err) for i := 0; i < 100; i++ { key := fmt.Sprintf("key%03d", i) // key000, key001, ..., key099 val := fmt.Sprintf("val%03d", i) // val000, val001, ..., val099 - cs.AddKVPair(storeKey1, store.KVPair{Key: []byte(key), Value: []byte(val)}) + require.NoError(t, batch.Set(storeKey1, []byte(key), []byte(val))) } - require.NoError(t, db.ApplyChangeset(1, cs)) + require.NoError(t, batch.Write()) // reverse iterator without an end key iter, err := db.ReverseIterator(storeKey1, 1, []byte("key000"), nil) @@ -106,15 +108,16 @@ func TestParallelWrites(t *testing.T) { go func(i int) { <-triggerStartCh defer wg.Done() - cs := store.NewChangeset(map[string]store.KVPairs{storeKey1: {}}) + batch, err := db.NewBatch(uint64(i + 1)) + require.NoError(t, err) for j := 0; j < kvCount; j++ { key := fmt.Sprintf("key-%d-%03d", i, j) val := fmt.Sprintf("val-%d-%03d", i, j) - cs.AddKVPair(storeKey1, store.KVPair{Key: []byte(key), Value: []byte(val)}) + require.NoError(t, batch.Set(storeKey1, []byte(key), []byte(val))) } - require.NoError(t, db.ApplyChangeset(uint64(i+1), cs)) + require.NoError(t, batch.Write()) }(i) } @@ -155,15 +158,16 @@ func TestParallelWriteAndPruning(t *testing.T) { <-triggerStartCh defer wg.Done() for i := 0; i < latestVersion; i++ { - cs := store.NewChangeset(map[string]store.KVPairs{storeKey1: {}}) + batch, err := db.NewBatch(uint64(i + 1)) + require.NoError(t, err) for j := 0; j < kvCount; j++ { key := fmt.Sprintf("key-%d-%03d", i, j) val := fmt.Sprintf("val-%d-%03d", i, j) - cs.AddKVPair(storeKey1, store.KVPair{Key: []byte(key), Value: []byte(val)}) + require.NoError(t, batch.Set(storeKey1, []byte(key), []byte(val))) } - require.NoError(t, db.ApplyChangeset(uint64(i+1), cs)) + require.NoError(t, batch.Write()) } }() // start a goroutine that prunes the database diff --git a/store/storage/storage_bench_test.go b/store/storage/storage_bench_test.go index 08ba4c8093ac..5639d5d4e84f 100644 --- a/store/storage/storage_bench_test.go +++ b/store/storage/storage_bench_test.go @@ -1,7 +1,7 @@ //go:build rocksdb // +build rocksdb -package storage +package storage_test import ( "bytes" @@ -13,21 +13,29 @@ import ( "github.com/stretchr/testify/require" "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/storage" "cosmossdk.io/store/v2/storage/pebbledb" "cosmossdk.io/store/v2/storage/rocksdb" "cosmossdk.io/store/v2/storage/sqlite" ) +const ( + storeKey1 = "store1" +) + var ( backends = map[string]func(dataDir string) (store.VersionedDatabase, error){ "rocksdb_versiondb_opts": func(dataDir string) (store.VersionedDatabase, error) { - return rocksdb.New(dataDir) + db, err := rocksdb.New(dataDir) + return storage.NewStorageStore(db), err }, "pebbledb_default_opts": func(dataDir string) (store.VersionedDatabase, error) { - return pebbledb.New(dataDir) + db, err := pebbledb.New(dataDir) + return storage.NewStorageStore(db), err }, "btree_sqlite": func(dataDir string) (store.VersionedDatabase, error) { - return sqlite.New(dataDir) + db, err := sqlite.New(dataDir) + return storage.NewStorageStore(db), err }, } rng = rand.New(rand.NewSource(567320)) diff --git a/store/storage/store.go b/store/storage/store.go new file mode 100644 index 000000000000..3a6fed65db2d --- /dev/null +++ b/store/storage/store.go @@ -0,0 +1,130 @@ +package storage + +import ( + "fmt" + + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/snapshots" +) + +const ( + // TODO: it is a random number, need to be tuned + defaultBatchBufferSize = 100000 +) + +var ( + _ store.VersionedDatabase = (*StorageStore)(nil) + _ snapshots.StorageSnapshotter = (*StorageStore)(nil) +) + +// StorageStore is a wrapper around the store.VersionedDatabase interface. +type StorageStore struct { + db Database +} + +// NewStorageStore returns a reference to a new StorageStore. +func NewStorageStore(db Database) *StorageStore { + return &StorageStore{ + db: db, + } +} + +// Has returns true if the key exists in the store. +func (ss *StorageStore) Has(storeKey string, version uint64, key []byte) (bool, error) { + return ss.db.Has(storeKey, version, key) +} + +// Get returns the value associated with the given key. +func (ss *StorageStore) Get(storeKey string, version uint64, key []byte) ([]byte, error) { + return ss.db.Get(storeKey, version, key) +} + +// ApplyChangeset applies the given changeset to the storage. +func (ss *StorageStore) ApplyChangeset(version uint64, cs *store.Changeset) error { + b, err := ss.db.NewBatch(version) + if err != nil { + return err + } + + for storeKey, pairs := range cs.Pairs { + for _, kvPair := range pairs { + if kvPair.Value == nil { + if err := b.Delete(storeKey, kvPair.Key); err != nil { + return err + } + } else { + if err := b.Set(storeKey, kvPair.Key, kvPair.Value); err != nil { + return err + } + } + } + } + + return b.Write() +} + +// GetLatestVersion returns the latest version of the store. +func (ss *StorageStore) GetLatestVersion() (uint64, error) { + return ss.db.GetLatestVersion() +} + +// SetLatestVersion sets the latest version of the store. +func (ss *StorageStore) SetLatestVersion(version uint64) error { + return ss.db.SetLatestVersion(version) +} + +// Iterator returns an iterator over the specified domain and prefix. +func (ss *StorageStore) Iterator(storeKey string, version uint64, start, end []byte) (store.Iterator, error) { + return ss.db.Iterator(storeKey, version, start, end) +} + +// ReverseIterator returns an iterator over the specified domain and prefix in reverse. +func (ss *StorageStore) ReverseIterator(storeKey string, version uint64, start, end []byte) (store.Iterator, error) { + return ss.db.ReverseIterator(storeKey, version, start, end) +} + +// Prune prunes the store up to the given version. +func (ss *StorageStore) Prune(version uint64) error { + return ss.db.Prune(version) +} + +// Restore restores the store from the given channel. +func (ss *StorageStore) Restore(version uint64, chStorage <-chan *store.KVPair) error { + latestVersion, err := ss.db.GetLatestVersion() + if err != nil { + return fmt.Errorf("failed to get latest version: %w", err) + } + if version <= latestVersion { + return fmt.Errorf("the snapshot version %d is not greater than latest version %d", version, latestVersion) + } + + b, err := ss.db.NewBatch(version) + if err != nil { + return err + } + + for kvPair := range chStorage { + if err := b.Set(kvPair.StoreKey, kvPair.Key, kvPair.Value); err != nil { + return err + } + + if b.Size() > defaultBatchBufferSize { + if err := b.Write(); err != nil { + return err + } + } + } + + if b.Size() > 0 { + if err := b.Write(); err != nil { + return err + } + } + + return ss.db.SetLatestVersion(version) +} + +// Close closes the store. +func (ss *StorageStore) Close() error { + return ss.db.Close() +} diff --git a/store/store.go b/store/store.go index 741a4c49b4f9..8bbeda7985ea 100644 --- a/store/store.go +++ b/store/store.go @@ -3,7 +3,8 @@ package store import ( "io" - ics23 "github.com/cosmos/ics23/go" + coreheader "cosmossdk.io/core/header" + "cosmossdk.io/store/v2/metrics" ) // StoreType defines a type of KVStore. @@ -19,14 +20,8 @@ const ( // RootStore defines an abstraction layer containing a State Storage (SS) engine // and one or more State Commitment (SC) engines. type RootStore interface { - // GetSCStore should return the SC backend for the given store key. A RootStore - // implementation may choose to ignore the store key in cases where only a single - // SC backend is used. - GetSCStore(storeKey string) Committer - // MountSCStore should mount the given SC backend for the given store key. For - // implementations that utilize a single SC backend, this method may be optional - // or a no-op. - MountSCStore(storeKey string, sc Committer) error + // GetSCStore should return the SC backend. + GetSCStore() Committer // GetKVStore returns the KVStore for the given store key. If an implementation // chooses to have a single SS backend, the store key may be ignored. GetKVStore(storeKey string) KVStore @@ -61,10 +56,13 @@ type RootStore interface { // GetLatestVersion returns the latest version, i.e. height, committed. GetLatestVersion() (uint64, error) + // SetInitialVersion sets the initial version on the RootStore. + SetInitialVersion(v uint64) error + // SetCommitHeader sets the commit header for the next commit. This call and // implementation is optional. However, it must be supported in cases where // queries based on block time need to be supported. - SetCommitHeader(h CommitHeader) + SetCommitHeader(h *coreheader.Info) // WorkingHash returns the current WIP commitment hash. Depending on the underlying // implementation, this may need to take the current changeset and write it to @@ -80,6 +78,12 @@ type RootStore interface { // be the same as the hash returned by WorkingHash() prior to calling Commit(). Commit() ([]byte, error) + // LastCommitID returns a CommitID pertaining to the last commitment. + LastCommitID() (CommitID, error) + + // SetMetrics sets the telemetry handler on the RootStore. + SetMetrics(m metrics.Metrics) + io.Closer } @@ -173,5 +177,5 @@ type QueryResult struct { Key []byte Value []byte Version uint64 - Proof *ics23.CommitmentProof + Proof CommitmentOp } diff --git a/telemetry/metrics.go b/telemetry/metrics.go index 690acf41d9c3..81708343aa0a 100644 --- a/telemetry/metrics.go +++ b/telemetry/metrics.go @@ -76,8 +76,8 @@ func New(cfg Config) (_ *Metrics, rerr error) { return nil, nil } - if numGlobalLables := len(cfg.GlobalLabels); numGlobalLables > 0 { - parsedGlobalLabels := make([]metrics.Label, numGlobalLables) + if numGlobalLabels := len(cfg.GlobalLabels); numGlobalLabels > 0 { + parsedGlobalLabels := make([]metrics.Label, numGlobalLabels) for i, gl := range cfg.GlobalLabels { parsedGlobalLabels[i] = NewLabel(gl[0], gl[1]) } diff --git a/tests/go.mod b/tests/go.mod index 5e48246f5dea..699cfa4475ac 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -14,7 +14,7 @@ require ( cosmossdk.io/store v1.0.1 cosmossdk.io/x/evidence v0.0.0-20230613133644-0a778132a60f cosmossdk.io/x/feegrant v0.0.0-20230613133644-0a778132a60f - cosmossdk.io/x/nft v0.0.0-20230613133644-0a778132a60f // indirect + cosmossdk.io/x/nft v0.0.0-20230613133644-0a778132a60f cosmossdk.io/x/protocolpool v0.0.0-20230925135524-a1bc045b3190 cosmossdk.io/x/tx v0.12.0 cosmossdk.io/x/upgrade v0.0.0-20230613133644-0a778132a60f @@ -34,6 +34,7 @@ require ( ) require ( + cosmossdk.io/x/accounts v0.0.0-20231013072015-ec9bcc41ef9c cosmossdk.io/x/auth v0.0.0-00010101000000-000000000000 cosmossdk.io/x/authz v0.0.0-00010101000000-000000000000 cosmossdk.io/x/bank v0.0.0-00010101000000-000000000000 @@ -54,7 +55,6 @@ require ( cloud.google.com/go/iam v1.1.5 // indirect cloud.google.com/go/storage v1.33.0 // indirect cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860 // indirect - cosmossdk.io/x/accounts v0.0.0-20231013072015-ec9bcc41ef9c // indirect cosmossdk.io/x/circuit v0.0.0-20230613133644-0a778132a60f // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect diff --git a/tools/confix/cmd/view.go b/tools/confix/cmd/view.go index bab2116c0453..0c845ce88015 100644 --- a/tools/confix/cmd/view.go +++ b/tools/confix/cmd/view.go @@ -12,7 +12,7 @@ import ( ) func ViewCommand() *cobra.Command { - flagOutputFomat := "output-format" + flagOutputFormat := "output-format" cmd := &cobra.Command{ Use: "view [config]", @@ -31,7 +31,7 @@ func ViewCommand() *cobra.Command { return err } - if format, _ := cmd.Flags().GetString(flagOutputFomat); format == "toml" { + if format, _ := cmd.Flags().GetString(flagOutputFormat); format == "toml" { cmd.Println(string(file)) return nil } @@ -48,7 +48,7 @@ func ViewCommand() *cobra.Command { } // output flag - cmd.Flags().String(flagOutputFomat, "toml", "Output format (json|toml)") + cmd.Flags().String(flagOutputFormat, "toml", "Output format (json|toml)") return cmd } diff --git a/x/accounts/go.mod b/x/accounts/go.mod index 0d619f0537ef..3ab99296e9f5 100644 --- a/x/accounts/go.mod +++ b/x/accounts/go.mod @@ -6,6 +6,7 @@ require ( cosmossdk.io/api v0.7.3-0.20231113122742-912390d5fc4a cosmossdk.io/collections v0.4.0 cosmossdk.io/core v0.12.1-0.20231114100755-569e3ff6a0d7 + github.com/cosmos/cosmos-proto v1.0.0-beta.3 github.com/cosmos/cosmos-sdk v0.51.0 github.com/cosmos/gogoproto v1.4.11 github.com/grpc-ecosystem/grpc-gateway v1.16.0 @@ -43,7 +44,6 @@ require ( github.com/cometbft/cometbft-db v0.8.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.0.0 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.3 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v1.0.0 // indirect diff --git a/x/circuit/README.md b/x/circuit/README.md index 7386680e3ef3..a8fb8f4facc7 100644 --- a/x/circuit/README.md +++ b/x/circuit/README.md @@ -4,8 +4,29 @@ Circuit Breaker is a module that is meant to avoid a chain needing to halt/shut down in the presence of a vulnerability, instead the module will allow specific messages or all messages to be disabled. When operating a chain, if it is app specific then a halt of the chain is less detrimental, but if there are applications built on top of the chain then halting is expensive due to the disturbance to applications. +## How it works + Circuit Breaker works with the idea that an address or set of addresses have the right to block messages from being executed and/or included in the mempool. Any address with a permission is able to reset the circuit breaker for the message. +The transactions are checked and can be rejected at two points: + +* In `CircuitBreakerDecorator` [ante handler](https://docs.cosmos.network/main/learn/advanced/baseapp#antehandler): + +```go reference +https://github.com/cosmos/cosmos-sdk/blob/x/circuit/v0.1.0/x/circuit/ante/circuit.go#L27-L41 +``` + +* With a [message router check](https://docs.cosmos.network/main/learn/advanced/baseapp#msg-service-router): + +```go reference +https://github.com/cosmos/cosmos-sdk/blob/v0.50.1/baseapp/msg_service_router.go#L104-L115 +``` + +:::note +The `CircuitBreakerDecorator` works for most use cases, but does not check the inner messages of a transaction. This some transactions (such as `x/authz` transactions or some `x/gov` transactions) may pass the ante handler. **This does not affect the circuit breaker** as the message router check will still fail the transaction. +This tradeoff is to avoid introducing more dependencies in the `x/circuit` module. Chains can re-define the `CircuitBreakerDecorator` to check for inner messages if they wish to do so. +::: + ## State ### Accounts @@ -38,7 +59,6 @@ type Access struct { } ``` - ### Disable List List of type urls that are disabled. @@ -108,7 +128,7 @@ This message is expected to fail if: * if the type url is not disabled -## Events - list and describe event tags +## Events The circuit module emits the following events: @@ -143,9 +163,41 @@ The circuit module emits the following events: | message | action | reset_circuit_breaker | -## Keys - list of key prefixes used by the circuit module +## Keys * `AccountPermissionPrefix` - `0x01` * `DisableListPrefix` - `0x02` -## Client - list and describe CLI commands and gRPC and REST endpoints +## Client + +### CLI + +`x/circuit` module client provides the following CLI commands: + +```shell +$ tx circuit +Transactions commands for the circuit module + +Usage: + simd tx circuit [flags] + simd tx circuit [command] + +Available Commands: + authorize Authorize an account to trip the circuit breaker. + disable Disable a message from being executed + reset Enable a message to be executed +``` + +```shell +$ query circuit +Querying commands for the circuit module + +Usage: + simd query circuit [flags] + simd query circuit [command] + +Available Commands: + account Query a specific account's permissions + accounts Query all account permissions + disabled-list Query a list of all disabled message types +``` diff --git a/x/staking/types/authz.go b/x/staking/types/authz.go index cacf5e225dbb..d3c7425d6ade 100644 --- a/x/staking/types/authz.go +++ b/x/staking/types/authz.go @@ -164,14 +164,24 @@ func validateAllowAndDenyValidators(allowed, denied []sdk.ValAddress) ([]string, allowedValidators := make([]string, len(allowed)) if len(allowed) > 0 { + foundAllowedValidators := make(map[string]bool, len(allowed)) for i, validator := range allowed { + if foundAllowedValidators[validator.String()] { + return nil, nil, sdkerrors.ErrInvalidRequest.Wrapf("duplicate allowed validator address: %s", validator.String()) + } + foundAllowedValidators[validator.String()] = true allowedValidators[i] = validator.String() } return allowedValidators, nil, nil } deniedValidators := make([]string, len(denied)) + foundDeniedValidators := make(map[string]bool, len(denied)) for i, validator := range denied { + if foundDeniedValidators[validator.String()] { + return nil, nil, sdkerrors.ErrInvalidRequest.Wrapf("duplicate denied validator address: %s", validator.String()) + } + foundDeniedValidators[validator.String()] = true deniedValidators[i] = validator.String() } diff --git a/x/staking/types/authz_test.go b/x/staking/types/authz_test.go index 8ef9a9b3868e..fa6185293ec5 100644 --- a/x/staking/types/authz_test.go +++ b/x/staking/types/authz_test.go @@ -42,6 +42,14 @@ func TestAuthzAuthorizations(t *testing.T) { _, err = stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{val1}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100) require.Error(t, err) + // error duplicate allow list + _, err = stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val1}, []sdk.ValAddress{}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100) + require.ErrorContains(t, err, "duplicate allowed validator address") + + // error duplicate denied list + _, err = stakingtypes.NewStakeAuthorization([]sdk.ValAddress{}, []sdk.ValAddress{val1, val1}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100) + require.ErrorContains(t, err, "duplicate denied validator address") + // verify MethodName undelAuth, _ := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_UNDELEGATE, &coin100) require.Equal(t, undelAuth.MsgTypeURL(), sdk.MsgTypeURL(&stakingtypes.MsgUndelegate{}))