diff --git a/cosmos-sdk-store/rootmulti/store.go b/cosmos-sdk-store/rootmulti/store.go index 04b41eb35..3712a698e 100755 --- a/cosmos-sdk-store/rootmulti/store.go +++ b/cosmos-sdk-store/rootmulti/store.go @@ -521,6 +521,7 @@ func (rs *Store) WorkingHash() []byte { if store.GetStoreType() != types.StoreTypeIAVL { continue } + fmt.Printf(">>>>>>> STORE OF %s <<<<<<<\n", key.Name()) if !rs.removalMap[key] { si := types.StoreInfo{ @@ -531,6 +532,7 @@ func (rs *Store) WorkingHash() []byte { } storeInfos = append(storeInfos, si) } + fmt.Printf("<<<<<<< END OF STORE OF %s >>>>>>>\n", key.Name()) } sort.SliceStable(storeInfos, func(i, j int) bool { diff --git a/deployment/dockerfiles/Dockerfile b/deployment/dockerfiles/Dockerfile index e3b7eee29..a20d53bea 100644 --- a/deployment/dockerfiles/Dockerfile +++ b/deployment/dockerfiles/Dockerfile @@ -127,6 +127,7 @@ COPY Makefile . RUN true COPY client client COPY cosmos-sdk-store cosmos-sdk-store +COPY iavl iavl RUN ln -s /usr/lib/x86_64-linux-gnu/liblz4.so /usr/local/lib/liblz4.so && ln -s /usr/lib/x86_64-linux-gnu/libzstd.so /usr/local/lib/libzstd.so diff --git a/go.mod b/go.mod index 8ebc5d25b..aa5aab976 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ replace ( cosmossdk.io/store => ./cosmos-sdk-store github.com/cometbft/cometbft => github.com/scrtlabs/tendermint v0.38.10-0.20240924173150-b47eda4ca72b github.com/cosmos/cosmos-sdk => github.com/scrtlabs/cosmos-sdk v0.46.0-beta2.0.20240917201403-3c75382e4a9d - github.com/cosmos/iavl => github.com/scrtlabs/iavl v1.1.2-secret.1 + github.com/cosmos/iavl => ./iavl github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 ) diff --git a/go.sum b/go.sum index 140b0c937..cd4149044 100644 --- a/go.sum +++ b/go.sum @@ -958,8 +958,6 @@ github.com/scrtlabs/cosmos-sdk v0.46.0-beta2.0.20240917201403-3c75382e4a9d h1:IW github.com/scrtlabs/cosmos-sdk v0.46.0-beta2.0.20240917201403-3c75382e4a9d/go.mod h1:9oxg/QW7VVnOzIip9DRJNAmSnzjSFwX3b350xv94D1I= github.com/scrtlabs/cosmos-sdk-api v0.7.5-secret.1 h1:4GLC5nv9pkCEUD4HpSpsnuDMYPT5Bly+IKPi/7H/ylk= github.com/scrtlabs/cosmos-sdk-api v0.7.5-secret.1/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= -github.com/scrtlabs/iavl v1.1.2-secret.1 h1:JX5h2U5Q/GxfVhUAm3rDgbaY2Rko7meCbVT2aJDigxw= -github.com/scrtlabs/iavl v1.1.2-secret.1/go.mod h1:jLeUvm6bGT1YutCaL2fIar/8vGUE8cPZvh/gXEWDaDM= github.com/scrtlabs/tendermint v0.38.10-0.20240924173150-b47eda4ca72b h1:vBdj5WibgXocSZLOST1NAr7V+c20ZHURJbdjuj47q/s= github.com/scrtlabs/tendermint v0.38.10-0.20240924173150-b47eda4ca72b/go.mod h1:AOeJIF/ftEqSywfsOcRexP0Zv2tNK78fdUcf7Vl75ns= github.com/scrtlabs/tm-secret-enclave v1.11.8 h1:fctIfJDHGl8D+fcXlZLX6S4yDeePIsuyzdG5HngFNPQ= diff --git a/iavl/.github/CODEOWNERS b/iavl/.github/CODEOWNERS new file mode 100644 index 000000000..b6f380a6d --- /dev/null +++ b/iavl/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Primary repo maintainers +* @cosmos/sdk-core-dev diff --git a/iavl/.github/dependabot.yml b/iavl/.github/dependabot.yml new file mode 100644 index 000000000..49052502f --- /dev/null +++ b/iavl/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 + - package-ecosystem: gomod + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 + labels: + - T:dependencies diff --git a/iavl/.github/stale.yml b/iavl/.github/stale.yml new file mode 100644 index 000000000..a72edc8e7 --- /dev/null +++ b/iavl/.github/stale.yml @@ -0,0 +1,47 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 9 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - S:wip + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +Limit to only `issues` or `pulls` +only: pulls + +Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +pulls: + daysUntilStale: 30 + markComment: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. \ No newline at end of file diff --git a/iavl/.github/workflows/benchmarks.yml b/iavl/.github/workflows/benchmarks.yml new file mode 100644 index 000000000..d95309e2b --- /dev/null +++ b/iavl/.github/workflows/benchmarks.yml @@ -0,0 +1,19 @@ +name: Benchmarks + +on: + push: + branches: + - master + pull_request: + +jobs: + Benchmarks: + runs-on: ubuntu-latest + container: ghcr.io/notional-labs/cosmos + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.20' # The Go version to download (if necessary) and use. + - name: run benchmarks + run: make bench diff --git a/iavl/.github/workflows/ci.yml b/iavl/.github/workflows/ci.yml new file mode 100644 index 000000000..a0ecac740 --- /dev/null +++ b/iavl/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Test +on: + push: + branches: + - master + pull_request: + +jobs: + Test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.20' # The Go version to download (if necessary) and use. + + # Some tests, notably TestRandomOperations, are extremely slow in CI + # with the race detector enabled, so we use -short when -race is + # enabled to reduce the number of slow tests, and then run without + # -short with -race disabled for a larger test set. The same tests + # are run, just with smaller data sets. + # + # We also do a 32-bit run. Even though this is executed on a 64-bit + # system, it will use 32-bit instructions and semantics (e.g. 32-bit + # integer overflow). + - name: test & coverage report creation + run: | + go test ./... -mod=readonly -timeout 10m -short -race -coverprofile=coverage.txt -covermode=atomic + go test ./... -mod=readonly -timeout 15m + GOARCH=386 go test ./... -mod=readonly -timeout 15m diff --git a/iavl/.github/workflows/codeql.yml b/iavl/.github/workflows/codeql.yml new file mode 100644 index 000000000..e26a31f29 --- /dev/null +++ b/iavl/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "15 1 * * 5" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ go ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/iavl/.github/workflows/lint-pr.yml b/iavl/.github/workflows/lint-pr.yml new file mode 100644 index 000000000..b337cbab3 --- /dev/null +++ b/iavl/.github/workflows/lint-pr.yml @@ -0,0 +1,22 @@ +name: "Lint PR" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + +jobs: + main: + permissions: + pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs + statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/iavl/.github/workflows/lint.yml b/iavl/.github/workflows/lint.yml new file mode 100644 index 000000000..1e03feecc --- /dev/null +++ b/iavl/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint +# Lint runs golangci-lint over the entire cosmos-sdk repository +# This workflow is run on every pull request and push to master +# The `golangci` will pass without running if no *.{go, mod, sum} files have been changed. +on: + pull_request: + push: + branches: + - master +jobs: + golangci: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: 🐿 Setup Golang + uses: actions/setup-go@v4 + with: + go-version: 1.21 + - name: golangci-lint + run: make lint diff --git a/iavl/.gitignore b/iavl/.gitignore new file mode 100644 index 000000000..7fff5ca4f --- /dev/null +++ b/iavl/.gitignore @@ -0,0 +1,18 @@ +vendor +.glide +*.swp +*.swo + +# created in test code +test.db + +# profiling data +*\.test +cpu*.out +mem*.out +cpu*.pdf +mem*.pdf + +# IDE files +.idea/* +.vscode/* diff --git a/iavl/.gitpod.yml b/iavl/.gitpod.yml new file mode 100644 index 000000000..dd6d37409 --- /dev/null +++ b/iavl/.gitpod.yml @@ -0,0 +1,2 @@ +image: ghcr.io/notional-labs/cosmos:latest + diff --git a/iavl/.golangci.yml b/iavl/.golangci.yml new file mode 100644 index 000000000..816d9c4a9 --- /dev/null +++ b/iavl/.golangci.yml @@ -0,0 +1,66 @@ +run: + tests: true + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + +linters: + disable-all: true + enable: + - bodyclose + - dogsled + - errcheck + - exportloopref + - goconst + - gocritic + - gofumpt + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - nolintlint + - prealloc + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + nolintlint: + allow-leading-space: true + require-explanation: false + require-specific: true + +issues: + exclude-rules: + - text: "Use of weak random number generator" + linters: + - gosec + - text: "comment on exported var" + linters: + - golint + - text: "don't use an underscore in package name" + linters: + - golint + - text: "should be written without leading space as" + linters: + - nolintlint + - text: "ST1003:" + linters: + - stylecheck + # FIXME: Disabled until golangci-lint updates stylecheck with this fix: + # https://github.com/dominikh/go-tools/issues/389 + - text: "ST1016:" + linters: + - stylecheck + - path: "migrations" + text: "SA1019:" + linters: + - staticcheck + + max-issues-per-linter: 10000 + max-same-issues: 10000 diff --git a/iavl/.mergify.yaml b/iavl/.mergify.yaml new file mode 100644 index 000000000..288d8aba0 --- /dev/null +++ b/iavl/.mergify.yaml @@ -0,0 +1,42 @@ +queue_rules: + - name: default + conditions: + - "#approved-reviews-by>1" + +pull_request_rules: + - name: automerge to main with label automerge and branch protection passing + conditions: + - "#approved-reviews-by>1" + - base=master + - label=A:automerge + actions: + queue: + name: default + method: squash + commit_message_template: | + {{ title }} (#{{ number }}) + {{ body }} + - name: backport patches to v0.19.x branch + conditions: + - base=master + - label=backport/v0.19.x + actions: + backport: + branches: + - release/v0.19.x + - name: backport patches to v0.20.x branch + conditions: + - base=master + - label=backport/v0.20.x + actions: + backport: + branches: + - release/v0.20.x + - name: backport patches to v1.x branch + conditions: + - base=master + - label=backport/v1.x + actions: + backport: + branches: + - release/v1.x.x diff --git a/iavl/CHANGELOG.md b/iavl/CHANGELOG.md new file mode 100644 index 000000000..9f2f67922 --- /dev/null +++ b/iavl/CHANGELOG.md @@ -0,0 +1,125 @@ +# Changelog + +## v1.1.2 April 8, 2024 + +### Bug Fixes + +- [#928](https://github.com/cosmos/iavl/pull/928) Fix the reformatted root node issue. + +## v1.1.1 March 16, 2024 + +### Bug Fixes + +- [#910](https://github.com/cosmos/iavl/pull/910) Fix the reference root format from (prefix, version) to (prefix, version, nonce) + +### Improvements + +- [#910](https://github.com/cosmos/iavl/pull/910) Async pruning of legacy orphan nodes. + +## v1.1.0 February 29, 2024 + +### API Breaking Changes + +- [#874](https://github.com/cosmos/iavl/pull/874) Decouple `cosmos-db` and implement own `db` package. + +## v1.0.1 February 16, 2024 + +### Improvements + +- [#876](https://github.com/cosmos/iavl/pull/876) Make pruning of legacy orphan nodes asynchronous. + +## v1.0.0 (October 30, 2023) + +### Improvements + +- [#695](https://github.com/cosmos/iavl/pull/695) Add API `SaveChangeSet` to save the changeset as a new version. +- [#703](https://github.com/cosmos/iavl/pull/703) New APIs `NewCompressExporter`/`NewCompressImporter` to support more compact snapshot format. +- [#729](https://github.com/cosmos/iavl/pull/729) Speedup Genesis writes for IAVL, by writing in small batches. +- [#726](https://github.com/cosmos/iavl/pull/726) Make `KVPair` and `ChangeSet` serializable with protobuf. +- [#718](https://github.com/cosmos/iavl/pull/718) Fix `traverseNodes` unexpected behaviour +- [#770](https://github.com/cosmos/iavl/pull/770) Add `WorkingVersion()int64` API. + +### Bug Fixes + +- [#773](https://github.com/cosmos/iavl/pull/773) Fix memory leak in `Import`. +- [#801](https://github.com/cosmos/iavl/pull/801) Fix rootKey empty check by len equals 0. +- [#805](https://github.com/cosmos/iavl/pull/805) Use `sync.Map` instead of map to prevent concurrent writes at the fast node level + +### Breaking Changes + +- [#735](https://github.com/cosmos/iavl/pull/735) Pass logger to `NodeDB`, `MutableTree` and `ImmutableTree` +- [#646](https://github.com/cosmos/iavl/pull/646) Remove the `orphans` from the storage +- [#777](https://github.com/cosmos/iavl/pull/777) Don't return errors from ImmutableTree.Hash, NewImmutableTree +- [#815](https://github.com/cosmos/iavl/pull/815) `NewMutableTreeWithOpts` was removed in favour of accepting options via a variadic in `NewMutableTree` +- [#815](https://github.com/cosmos/iavl/pull/815) `NewImmutableTreeWithOpts` is removed in favour of accepting options via a variadic in `NewImmutableTree` +- [#646](https://github.com/cosmos/iavl/pull/646) Remove the `DeleteVersion`, `DeleteVersions`, `DeleteVersionsRange` and introduce a new endpoint of `DeleteVersionsTo` instead + +## 0.20.0 (March 14, 2023) + +### Breaking Changes + +- [#586](https://github.com/cosmos/iavl/pull/586) Remove the `RangeProof` and refactor the ics23_proof to use the internal methods. + +## 0.19.5 (Februrary 23, 2022) + +### Breaking Changes + +- [#622](https://github.com/cosmos/iavl/pull/622) `export/newExporter()` and `ImmutableTree.Export()` returns error for nil arguements + +- [#640](https://github.com/cosmos/iavl/pull/640) commit `NodeDB` batch in `LoadVersionForOverwriting`. +- [#636](https://github.com/cosmos/iavl/pull/636) Speed up rollback method: `LoadVersionForOverwriting`. +- [#654](https://github.com/cosmos/iavl/pull/654) Add API `TraverseStateChanges` to extract state changes from iavl versions. +- [#638](https://github.com/cosmos/iavl/pull/638) Make LazyLoadVersion check the opts.InitialVersion, add API `LazyLoadVersionForOverwriting`. + +## 0.19.4 (October 28, 2022) + +- [#599](https://github.com/cosmos/iavl/pull/599) Populate ImmutableTree creation in copy function with missing field +- [#589](https://github.com/cosmos/iavl/pull/589) Wrap `tree.addUnsavedRemoval()` with missing `if !tree.skipFastStorageUpgrade` statement + +## 0.19.3 (October 8, 2022) + +- `ProofInner.Hash()` prevents both right and left from both being set. Only one is allowed to be set. + +## 0.19.2 (October 6, 2022) + +- [#547](https://github.com/cosmos/iavl/pull/547) Implement `skipFastStorageUpgrade` in order to skip fast storage upgrade and usage. +- [#531](https://github.com/cosmos/iavl/pull/531) Upgrade to fast storage in batches. + +## 0.19.1 (August 3, 2022) + +### Improvements + +- [#525](https://github.com/cosmos/iavl/pull/525) Optimization: use fast unsafe bytes->string conversion. +- [#506](https://github.com/cosmos/iavl/pull/506) Implement cache abstraction. + +### Bug Fixes + +- [#524](https://github.com/cosmos/iavl/pull/524) Fix: `MutableTree.Get`. + +## 0.19.0 (July 6, 2022) + +### Breaking Changes + +- [#514](https://github.com/cosmos/iavl/pull/514) Downgrade Tendermint to 0.34.x +- [#500](https://github.com/cosmos/iavl/pull/500) Return errors instead of panicking. + +### Improvements + +- [#514](https://github.com/cosmos/iavl/pull/514) Use Go v1.18 + +## 0.18.0 (March 10, 2022) + +### Breaking Changes + +- Bumped Tendermint to 0.35.1 + +### Improvements + +- [\#468](https://github.com/cosmos/iavl/pull/468) Fast storage optimization for queries and iterations +- [\#452](https://github.com/cosmos/iavl/pull/452) Optimization: remove unnecessary (\*bytes.Buffer).Reset right after creating buffer. +- [\#445](https://github.com/cosmos/iavl/pull/445) Bump github.com/tendermint/tendermint to v0.35.0 +- [\#453](https://github.com/cosmos/iavl/pull/453),[\#456](https://github.com/cosmos/iavl/pull/456) Optimization: buffer reuse +- [\#474](https://github.com/cosmos/iavl/pull/474) bump github.com/confio/ics23 to v0.7 +- [\#475](https://github.com/cosmos/iavl/pull/475) Use go v1.17 + +For previous changelogs visit: diff --git a/iavl/CONTRIBUTING.md b/iavl/CONTRIBUTING.md new file mode 100644 index 000000000..51d859380 --- /dev/null +++ b/iavl/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +Thank you for considering making contributions to IAVL+! +This repository follows the [contribution guidelines] of tendermint and the corresponding [coding repo]. +Please take a look if you are not already familiar with those. + +[contribution guidelines]: https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md diff --git a/iavl/LICENSE b/iavl/LICENSE new file mode 100644 index 000000000..084d3f719 --- /dev/null +++ b/iavl/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright Cosmos-IAVL Authors + Copyright 2015 Tendermint + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/iavl/Makefile b/iavl/Makefile new file mode 100644 index 000000000..73e05df27 --- /dev/null +++ b/iavl/Makefile @@ -0,0 +1,91 @@ +VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') +COMMIT := $(shell git log -1 --format='%H') +BRANCH=$(shell git rev-parse --abbrev-ref HEAD) +DOCKER_BUF := docker run -v $(shell pwd):/workspace --workdir /workspace bufbuild/buf +DOCKER := $(shell which docker) +HTTPS_GIT := https://github.com/cosmos/iavl.git + +PDFFLAGS := -pdf --nodefraction=0.1 +CMDFLAGS := -ldflags -X TENDERMINT_IAVL_COLORS_ON=on +LDFLAGS := -ldflags "-X github.com/cosmos/iavl.Version=$(VERSION) -X github.com/cosmos/iavl.Commit=$(COMMIT) -X github.com/cosmos/iavl.Branch=$(BRANCH)" + +all: lint test install + +install: +ifeq ($(COLORS_ON),) + go install ./cmd/iaviewer +else + go install $(CMDFLAGS) ./cmd/iaviewer +endif +.PHONY: install + +test-short: + @echo "--> Running go test" + @go test ./... $(LDFLAGS) -v --race --short +.PHONY: test-short + +test: + @echo "--> Running go test" + @go test ./... $(LDFLAGS) -v +.PHONY: test + +format: + find . -name '*.go' -type f -not -path "*.git*" -not -name '*.pb.go' -not -name '*pb_test.go' | xargs gofmt -w -s + find . -name '*.go' -type f -not -path "*.git*" -not -name '*.pb.go' -not -name '*pb_test.go' | xargs goimports -format +.PHONY: format + +# look into .golangci.yml for enabling / disabling linters +golangci_lint_cmd=golangci-lint +golangci_version=v1.55.2 + +lint: + @echo "--> Running linter" + @go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version) + @$(golangci_lint_cmd) run --timeout=10m + +lint-fix: + @echo "--> Running linter" + @go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version) + @$(golangci_lint_cmd) run --fix --out-format=tab --issues-exit-code=0 + +# bench is the basic tests that shouldn't crash an aws instance +bench: + cd benchmarks && \ + go test $(LDFLAGS) -tags pebbledb -run=NOTEST -bench=Small . && \ + go test $(LDFLAGS) -tags pebbledb -run=NOTEST -bench=Medium . && \ + go test $(LDFLAGS) -run=NOTEST -bench=RandomBytes . +.PHONY: bench + +# fullbench is extra tests needing lots of memory and to run locally +fullbench: + cd benchmarks && \ + go test $(LDFLAGS) -run=NOTEST -bench=RandomBytes . && \ + go test $(LDFLAGS) -tags rocksdb,pebbledb -run=NOTEST -bench=Small . && \ + go test $(LDFLAGS) -tags rocksdb,pebbledb -run=NOTEST -bench=Medium . && \ + go test $(LDFLAGS) -tags rocksdb,pebbledb -run=NOTEST -timeout=30m -bench=Large . && \ + go test $(LDFLAGS) -run=NOTEST -bench=Mem . && \ + go test $(LDFLAGS) -run=NOTEST -timeout=60m -bench=LevelDB . +.PHONY: fullbench + +# note that this just profiles the in-memory version, not persistence +profile: + cd benchmarks && \ + go test $(LDFLAGS) -bench=Mem -cpuprofile=cpu.out -memprofile=mem.out . && \ + go tool pprof ${PDFFLAGS} benchmarks.test cpu.out > cpu.pdf && \ + go tool pprof --alloc_space ${PDFFLAGS} benchmarks.test mem.out > mem_space.pdf && \ + go tool pprof --alloc_objects ${PDFFLAGS} benchmarks.test mem.out > mem_obj.pdf +.PHONY: profile + +explorecpu: + cd benchmarks && \ + go tool pprof benchmarks.test cpu.out +.PHONY: explorecpu + +exploremem: + cd benchmarks && \ + go tool pprof --alloc_objects benchmarks.test mem.out +.PHONY: exploremem + +delve: + dlv test ./benchmarks -- -test.bench=. +.PHONY: delve diff --git a/iavl/PERFORMANCE.md b/iavl/PERFORMANCE.md new file mode 100644 index 000000000..b1acdd932 --- /dev/null +++ b/iavl/PERFORMANCE.md @@ -0,0 +1,111 @@ +# Performance + +After some discussion with Jae on the usability, it seems performance is a big concern. If every write takes around 1ms, that puts a serious upper limit on the speed of the consensus engine (especially since with the check/tx dichotomy, we need at least two writes (to cache, only one to disk) and likely two or more queries to handle any transaction). + +As Jae notes: for CheckTx, a copy of IAVLTree doesn't need to be saved. During CheckTx it'll load inner nodes into the cache. The cache is shared w/ the AppendTx state IAVLTree, so during AppendTx we should save some time. There would only be 1 set of writes. Also, there's quite a bit of free time in between blocks as provided by Tendermint, during which CheckTx can run priming the cache, so hopefully this helps as well. + +Jae: That said, I'm not sure exactly what the tx throughput would be during normal running times. I'm hoping that we can have e.g. 3 second blocks w/ say over a hundred txs per sec per block w/ 1 million items. That will get us through for some time, but that time is limited. + +Ethan: I agree, and think this works now with goleveldb backing on most host machines. For public chains, maybe it is desired to push 1000 tx every 3 sec to a block, with a db size of 1 billion items. 10x the throughput with 1000x the data. That could be a long-term goal, and would scale to the cosmos and beyond. + +## Plan + +For any goal, we need some clear steps. + +1) Cleanup code, and write some more benchmark cases to capture "realistic" usage +2) Run tests on various hardware to see the best performing backing stores +3) Do profiling on the best performance to see if there are any easy performance gains +4) (Possibly) Write another implementation of merkle.Tree to improve all the memory overhead, consider CPU cache, etc.... +5) (Possibly) Write another backend datastore to persist the tree in a more efficient way + +The rest of this document is the planned or completed actions for the above-listed steps. + +## Cleanup + +Done in branch `cleanup_deps`: + * Fixed up dependeny management (tmlibs/db etc in glide/vendor) + * Updated Makefile (test, bench, get_deps) + * Fixed broken code - `looper.go` and one benchmark didn't run + +Benchmarks should be parameterized on: + 1) storage implementation + 2) initial data size + 3) length of keys + 4) length of data + 5) block size (frequency of copy/hash...) +Thus, we would see the same benchmark run against memdb with 100K items, goleveldb with 100K, leveldb with 100K, memdb with 10K, goleveldb with 10K... + +Scenarios to run after db is set up. + * Pure query time (known/hits, vs. random/misses) + * Write timing (known/updates, vs. random/inserts) + * Delete timing (existing keys only) + * TMSP Usage: + * For each block size: + * 2x copy "last commit" -> check and real + * repeat for each tx: + * (50% update + 50% insert?) + * query + insert/update in check + * query + insert/update in real + * get hash + * save real + * real -> "last commit" + + +## Benchmarks + +After writing the benchmarks, we can run them under various environments and store the results under benchmarks directory. Some useful environments to test: + + * Dev machines + * Digital ocean small/large machine + * Various AWS setups + +Please run the benchmark on more machines and add the result. Just type: `make record` in the directory and wait a (long) while (with little other load on the machine). + +This will require also a quick setup script to install go and run tests in these environments. Maybe some scripts even. Also, this will produce a lot of files and we may have to graph them to see something useful... + +But for starting, my laptop, and one digital ocean and one aws server should be sufficient. At least to find the winner, before profiling. + + +## Profiling + +Once we figure out which current implementation looks fastest, let's profile it to make it even faster. It is great to optimize the memdb code to really speed up the hashing and tree-building logic. And then focus on the backend implementation to optimize the disk storage, which will be the next major pain point. + +Some guides: + + * [Profiling benchmarks locally](https://medium.com/@hackintoshrao/daily-code-optimization-using-benchmarks-and-profiling-in-golang-gophercon-india-2016-talk-874c8b4dc3c5#.jmnd8w2qr) + * [On optimizing memory](https://signalfx.com/blog/a-pattern-for-optimizing-go-2/) + * [Profiling running programs](http://blog.ralch.com/tutorial/golang-performance-and-memory-analysis/) + * [Dave Chenny's profiler pkg](https://github.com/pkg/profile) + +Some ideas for speedups: + + * [Speedup SHA256 100x on ARM](https://blog.minio.io/accelerating-sha256-by-100x-in-golang-on-arm-1517225f5ff4#.pybt7bb3w) + * [Faster SHA256 golang implementation](https://github.com/minio/sha256-simd) + * [Data structure alignment](http://stackoverflow.com/questions/39063530/optimising-datastructure-word-alignment-padding-in-golang) + * [Slice alignment](http://blog.chewxy.com/2016/07/25/on-the-memory-alignment-of-go-slice-values/) + * [Tool to analyze your structs](https://github.com/dominikh/go-structlayout) + +## Tree Re-implementation + +If we want to copy lots of objects, it becomes better to think of using memcpy on large (eg. 4-16KB) buffers than copying individual structs. We also could allocate arrays of structs and align them to remove a lot of memory management and gc overhead. That means going down to some C-level coding... + +Some links for thought: + + * [Array representation of a binary tree](http://www.cse.hut.fi/en/research/SVG/TRAKLA2/tutorials/heap_tutorial/taulukkona.html) + * [Memcpy buffer size timing](http://stackoverflow.com/questions/21038965/why-does-the-speed-of-memcpy-drop-dramatically-every-4kb) + * [Calling memcpy from go](https://github.com/jsgilmore/shm/blob/master/memcpy.go) + * [Unsafe docs](https://godoc.org/unsafe) + * [...and how to use it](https://copyninja.info/blog/workaround-gotypesystems.html) + * [Or maybe just plain copy...](https://godoc.org/builtin#copy) + +## Backend implementation + +Storing each link in the tree in leveldb treats each node as an isolated item. Since we know some usage patterns (when a parent is hit, very likely one child will be hit), we could try to organize the memory and disk location of the nodes ourselves to make it more efficient. Or course, this could be a long, slippery slope. + +Inspired by the [Array representation](http://www.cse.hut.fi/en/research/SVG/TRAKLA2/tutorials/heap_tutorial/taulukkona.html) link above, we could consider other layouts for the nodes. For example, rather than store them alone, or the entire tree in one big array, the nodes could be placed in groups of 15 based on the parent (parent and 3 generations of children). Then we have 4 levels before jumping to another location. Maybe we just store this larger chunk as one leveldb location, or really try to do the mmap ourselves... + +In any case, assuming around 100 bytes for one non-leaf node (3 sha hashes, plus prefix, plus other data), 15 nodes would be a little less than 2K, maybe even go one more level to 31 nodes and 3-4KB, where we could take best advantage of the memory/disk page size. + +Some links for thought: + + * [Memory mapped files](https://github.com/edsrzf/mmap-go) diff --git a/iavl/POEM b/iavl/POEM new file mode 100644 index 000000000..f361067a4 --- /dev/null +++ b/iavl/POEM @@ -0,0 +1,29 @@ +writing down, my checksum +waiting for the, data to come +no need to pray for integrity +thats cuz I use, a merkle tree + +grab the root, with a quick hash run +if the hash works out, +it must have been done + +theres no need, for trust to arise +thanks to the crypto +now that I can merkleyes + +take that data, merklize +ye, I merklize ... + +then the truth, begins to shine +the inverse of a hash, you will never find +and as I watch, the dataset grow +producing a proof, is never slow + +Where do I find, the will to hash +How do I teach it? +It doesn't pay in cash +Bitcoin, here, I've realized +Thats what I need now, +cuz real currencies merklize + +-EB diff --git a/iavl/README.md b/iavl/README.md new file mode 100644 index 000000000..419830196 --- /dev/null +++ b/iavl/README.md @@ -0,0 +1,32 @@ +# IAVL+ Tree + +[![version](https://img.shields.io/github/tag/cosmos/iavl.svg)](https://github.com/cosmos/iavl/releases/latest) +[![license](https://img.shields.io/github/license/cosmos/iavl.svg)](https://github.com/cosmos/iavl/blob/master/LICENSE) +[![API Reference](https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667)](https://pkg.go.dev/github.com/cosmos/iavl) +![Lint](https://github.com/cosmos/iavl/workflows/Lint/badge.svg?branch=master) +![Test](https://github.com/cosmos/iavl/workflows/Test/badge.svg?branch=master) +[![Discord chat](https://img.shields.io/discord/669268347736686612.svg)](https://discord.gg/AzefAFd) + +Note: **Requires Go 1.18+** + +A versioned, snapshottable (immutable) AVL+ tree for persistent data. + +[Benchmarks](https://dashboard.bencher.orijtech.com/graphs?repo=https%3A%2F%2Fgithub.com%2Fcosmos%2Fiavl.git) + +The purpose of this data structure is to provide persistent storage for key-value pairs (say to store account balances) such that a deterministic merkle root hash can be computed. The tree is balanced using a variant of the [AVL algorithm](http://en.wikipedia.org/wiki/AVL_tree) so all operations are O(log(n)). + +Nodes of this tree are immutable and indexed by their hash. Thus any node serves as an immutable snapshot which lets us stage uncommitted transactions from the mempool cheaply, and we can instantly roll back to the last committed state to process transactions of a newly committed block (which may not be the same set of transactions as those from the mempool). + +In an AVL tree, the heights of the two child subtrees of any node differ by at most one. Whenever this condition is violated upon an update, the tree is rebalanced by creating O(log(n)) new nodes that point to unmodified nodes of the old tree. In the original AVL algorithm, inner nodes can also hold key-value pairs. The AVL+ algorithm (note the plus) modifies the AVL algorithm to keep all values on leaf nodes, while only using branch-nodes to store keys. This simplifies the algorithm while keeping the merkle hash trail short. + +In Ethereum, the analog is [Patricia tries](http://en.wikipedia.org/wiki/Radix_tree). There are tradeoffs. Keys do not need to be hashed prior to insertion in IAVL+ trees, so this provides faster iteration in the key space which may benefit some applications. The logic is simpler to implement, requiring only two types of nodes -- inner nodes and leaf nodes. On the other hand, while IAVL+ trees provide a deterministic merkle root hash, it depends on the order of transactions. In practice this shouldn't be a problem, since you can efficiently encode the tree structure when serializing the tree contents. + +## IAVL x Cosmos SDK + +| IAVL | DB Interface | Cosmos SDK | +| -------------------------------------------------------------- | -------------------------------------------------------- | ---------------- | +| [v0.19.x](https://github.com/cosmos/iavl/tree/release/v0.19.x) | [`tm-db`](https://github.com/tendermint/tm-db) | v0.45.x, v0.46.x | +| [v0.20.x](https://github.com/cosmos/iavl/tree/release/v0.20.x) | [`cometbft-db`](https://github.com/cometbft/cometbft-db) | v0.47.x | +| [v1.x.x](https://github.com/cosmos/iavl/tree/release/v1.x.x) | [`cosmos-db`](https://github.com/cosmos/cosmos-db) | - | + +NOTE: In the past, a v0.21.x release was published, but never used in production. It was retracted to avoid confusion. diff --git a/iavl/SECURITY.md b/iavl/SECURITY.md new file mode 100644 index 000000000..636d34e85 --- /dev/null +++ b/iavl/SECURITY.md @@ -0,0 +1,79 @@ +# Coordinated Vulnerability Disclosure Policy + +The Cosmos ecosystem believes that strong security is a blend of highly +technical security researchers who care about security and the forward +progression of the ecosystem and the attentiveness and openness of Cosmos core +contributors to help continually secure our operations. + +> **IMPORTANT**: *DO NOT* open public issues on this repository for security +> vulnerabilities. + +## Scope + +| Scope | +|-----------------------| +| last release (tagged) | +| main branch | + +The latest **release tag** of this repository is supported for security updates +as well as the **main** branch. Security vulnerabilities should be reported if +the vulnerability can be reproduced on either one of those. + +## Reporting a Vulnerability + +| Reporting methods | +|---------------------------------------------------------------| +| [GitHub Private Vulnerability Reporting][gh-private-advisory] | +| [HackerOne bug bounty program][h1] | + +All security vulnerabilities can be reported under GitHub's [Private +vulnerability reporting][gh-private-advisory] system. This will open a private +issue for the developers. Try to fill in as much of the questions as possible. +If you are not familiar with the CVSS system for assessing vulnerabilities, just +use the Low/High/Critical severity ratings. A partially filled in report for a +critical vulnerability is still better than no report at all. + +Vulnerabilities associated with the **Go, Rust or Protobuf code** of the +repository may be eligible for a [bug bounty][h1]. Please see the bug bounty +page for more details on submissions and rewards. If you think the vulnerability +is eligible for a payout, **report on HackerOne first**. + +Vulnerabilities in services and their source codes (JavaScript, web page, Google +Workspace) are not in scope for the bug bounty program, but they are welcome to +be reported in GitHub. + +### Guidelines + +We require that all researchers: + +* Abide by this policy to disclose vulnerabilities, and avoid posting + vulnerability information in public places, including GitHub, Discord, + Telegram, and Twitter. +* Make every effort to avoid privacy violations, degradation of user experience, + disruption to production systems (including but not limited to the Cosmos + Hub), and destruction of data. +* Keep any information about vulnerabilities that you’ve discovered confidential + between yourself and the Cosmos engineering team until the issue has been + resolved and disclosed. +* Avoid posting personally identifiable information, privately or publicly. + +If you follow these guidelines when reporting an issue to us, we commit to: + +* Not pursue or support any legal action related to your research on this + vulnerability +* Work with you to understand, resolve and ultimately disclose the issue in a + timely fashion + +### More information + +* See [TIMELINE.md] for an example timeline of a disclosure. +* See [DISCLOSURE.md] to see more into the inner workings of the disclosure + process. +* See [EXAMPLES.md] for some of the examples that we are interested in for the + bug bounty program. + +[gh-private-advisory]: /../../security/advisories/new +[h1]: https://hackerone.com/cosmos +[TIMELINE.md]: https://github.com/cosmos/security/blob/main/TIMELINE.md +[DISCLOSURE.md]: https://github.com/cosmos/security/blob/main/DISCLOSURE.md +[EXAMPLES.md]: https://github.com/cosmos/security/blob/main/EXAMPLES.md diff --git a/iavl/basic_test.go b/iavl/basic_test.go new file mode 100644 index 000000000..88134d65d --- /dev/null +++ b/iavl/basic_test.go @@ -0,0 +1,533 @@ +// nolint: errcheck +package iavl + +import ( + "encoding/hex" + mrand "math/rand" + "sort" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +func TestBasic(t *testing.T) { + tree := getTestTree(0) + up, err := tree.Set([]byte("1"), []byte("one")) + require.NoError(t, err) + if up { + t.Error("Did not expect an update (should have been create)") + } + up, err = tree.Set([]byte("2"), []byte("two")) + require.NoError(t, err) + if up { + t.Error("Did not expect an update (should have been create)") + } + up, err = tree.Set([]byte("2"), []byte("TWO")) + require.NoError(t, err) + if !up { + t.Error("Expected an update") + } + up, err = tree.Set([]byte("5"), []byte("five")) + require.NoError(t, err) + if up { + t.Error("Did not expect an update (should have been create)") + } + + // Test 0x00 + { + key := []byte{0x00} + expected := "" + + idx, val, err := tree.GetWithIndex(key) + require.NoError(t, err) + if val != nil { + t.Error("Expected no value to exist") + } + if idx != 0 { + t.Errorf("Unexpected idx %x", idx) + } + if string(val) != expected { + t.Errorf("Unexpected value %s", val) + } + + val, _ = tree.Get(key) + if val != nil { + t.Error("Fast method - expected no value to exist") + } + if string(val) != expected { + t.Errorf("Fast method - Unexpected value %s", val) + } + } + + // Test "1" + { + key := []byte("1") + expected := "one" + + idx, val, err := tree.GetWithIndex(key) + require.NoError(t, err) + if val == nil { + t.Error("Expected value to exist") + } + if idx != 0 { + t.Errorf("Unexpected idx %x", idx) + } + if string(val) != expected { + t.Errorf("Unexpected value %s", val) + } + + val, err = tree.Get(key) + require.NoError(t, err) + if val == nil { + t.Error("Fast method - expected value to exist") + } + if string(val) != expected { + t.Errorf("Fast method - Unexpected value %s", val) + } + } + + // Test "2" + { + key := []byte("2") + expected := "TWO" + + idx, val, err := tree.GetWithIndex(key) + require.NoError(t, err) + if val == nil { + t.Error("Expected value to exist") + } + if idx != 1 { + t.Errorf("Unexpected idx %x", idx) + } + if string(val) != expected { + t.Errorf("Unexpected value %s", val) + } + + val, _ = tree.Get(key) + if val == nil { + t.Error("Fast method - expected value to exist") + } + if string(val) != expected { + t.Errorf("Fast method - Unexpected value %s", val) + } + } + + // Test "4" + { + key := []byte("4") + expected := "" + + idx, val, err := tree.GetWithIndex(key) + require.NoError(t, err) + if val != nil { + t.Error("Expected no value to exist") + } + if idx != 2 { + t.Errorf("Unexpected idx %x", idx) + } + if string(val) != expected { + t.Errorf("Unexpected value %s", val) + } + + val, _ = tree.Get(key) + if val != nil { + t.Error("Fast method - expected no value to exist") + } + if string(val) != expected { + t.Errorf("Fast method - Unexpected value %s", val) + } + } + + // Test "6" + { + key := []byte("6") + expected := "" + + idx, val, err := tree.GetWithIndex(key) + require.NoError(t, err) + if val != nil { + t.Error("Expected no value to exist") + } + if idx != 3 { + t.Errorf("Unexpected idx %x", idx) + } + if string(val) != expected { + t.Errorf("Unexpected value %s", val) + } + + val, _ = tree.Get(key) + if val != nil { + t.Error("Fast method - expected no value to exist") + } + if string(val) != expected { + t.Errorf("Fast method - Unexpected value %s", val) + } + } +} + +func TestUnit(t *testing.T) { + expectSet := func(tree *MutableTree, i int, repr string) { + tree.SaveVersion() + updated, err := tree.Set(i2b(i), []byte{}) + require.NoError(t, err) + // ensure node was added & structure is as expected. + if updated || P(tree.root, tree.ImmutableTree) != repr { + t.Fatalf("Adding %v to %v:\nExpected %v\nUnexpectedly got %v updated:%v", + i, P(tree.lastSaved.root, tree.lastSaved), repr, P(tree.root, tree.ImmutableTree), updated) + } + tree.ImmutableTree = tree.lastSaved.clone() + } + + expectRemove := func(tree *MutableTree, i int, repr string) { + tree.SaveVersion() + value, removed, err := tree.Remove(i2b(i)) + require.NoError(t, err) + // ensure node was added & structure is as expected. + if len(value) != 0 || !removed || P(tree.root, tree.ImmutableTree) != repr { + t.Fatalf("Removing %v from %v:\nExpected %v\nUnexpectedly got %v value:%v removed:%v", + i, P(tree.lastSaved.root, tree.lastSaved), repr, P(tree.root, tree.ImmutableTree), value, removed) + } + tree.ImmutableTree = tree.lastSaved.clone() + } + + // Test Set cases: + + // Case 1: + t1, err := T(N(4, 20)) + + require.NoError(t, err) + expectSet(t1, 8, "((4 8) 20)") + expectSet(t1, 25, "(4 (20 25))") + + t2, err := T(N(4, N(20, 25))) + + require.NoError(t, err) + expectSet(t2, 8, "((4 8) (20 25))") + expectSet(t2, 30, "((4 20) (25 30))") + + t3, err := T(N(N(1, 2), 6)) + + require.NoError(t, err) + expectSet(t3, 4, "((1 2) (4 6))") + expectSet(t3, 8, "((1 2) (6 8))") + + t4, err := T(N(N(1, 2), N(N(5, 6), N(7, 9)))) + + require.NoError(t, err) + expectSet(t4, 8, "(((1 2) (5 6)) ((7 8) 9))") + expectSet(t4, 10, "(((1 2) (5 6)) (7 (9 10)))") + + // Test Remove cases: + + t10, err := T(N(N(1, 2), 3)) + + require.NoError(t, err) + expectRemove(t10, 2, "(1 3)") + expectRemove(t10, 3, "(1 2)") + + t11, err := T(N(N(N(1, 2), 3), N(4, 5))) + + require.NoError(t, err) + expectRemove(t11, 4, "((1 2) (3 5))") + expectRemove(t11, 3, "((1 2) (4 5))") +} + +func TestRemove(_ *testing.T) { + keyLen, dataLen := 16, 40 + + size := 10000 + t1 := getTestTree(size) + + // insert a bunch of random nodes + keys := make([][]byte, size) + l := int32(len(keys)) + for i := 0; i < size; i++ { + key := iavlrand.RandBytes(keyLen) + t1.Set(key, iavlrand.RandBytes(dataLen)) + keys[i] = key + } + + for i := 0; i < 10; i++ { + step := 50 * i + // remove a bunch of existing keys (may have been deleted twice) + for j := 0; j < step; j++ { + key := keys[mrand.Int31n(l)] + t1.Remove(key) + } + t1.SaveVersion() + } +} + +func TestIntegration(t *testing.T) { + type record struct { + key string + value string + } + + records := make([]*record, 400) + tree := getTestTree(0) + + randomRecord := func() *record { + return &record{randstr(20), randstr(20)} + } + + for i := range records { + r := randomRecord() + records[i] = r + updated, err := tree.Set([]byte(r.key), []byte{}) + require.NoError(t, err) + if updated { + t.Error("should have not been updated") + } + updated, err = tree.Set([]byte(r.key), []byte(r.value)) + require.NoError(t, err) + if !updated { + t.Error("should have been updated") + } + if tree.Size() != int64(i+1) { + t.Error("size was wrong", tree.Size(), i+1) + } + } + + for _, r := range records { + has, err := tree.Has([]byte(r.key)) + require.NoError(t, err) + if !has { + t.Error("Missing key", r.key) + } + + has, err = tree.Has([]byte(randstr(12))) + require.NoError(t, err) + if has { + t.Error("Table has extra key") + } + + val, err := tree.Get([]byte(r.key)) + require.NoError(t, err) + if string(val) != r.value { + t.Error("wrong value") + } + } + + for i, x := range records { + if val, removed, err := tree.Remove([]byte(x.key)); err != nil { + require.NoError(t, err) + } else if !removed { + t.Error("Wasn't removed") + } else if string(val) != x.value { + t.Error("Wrong value") + } + for _, r := range records[i+1:] { + has, err := tree.Has([]byte(r.key)) + require.NoError(t, err) + if !has { + t.Error("Missing key", r.key) + } + + has, err = tree.Has([]byte(randstr(12))) + require.NoError(t, err) + if has { + t.Error("Table has extra key") + } + + val, err := tree.Get([]byte(r.key)) + require.NoError(t, err) + if string(val) != r.value { + t.Error("wrong value") + } + } + if tree.Size() != int64(len(records)-(i+1)) { + t.Error("size was wrong", tree.Size(), (len(records) - (i + 1))) + } + } +} + +func TestIterateRange(t *testing.T) { + type record struct { + key string + value string + } + + records := []record{ + {"abc", "123"}, + {"low", "high"}, + {"fan", "456"}, + {"foo", "a"}, + {"foobaz", "c"}, + {"good", "bye"}, + {"foobang", "d"}, + {"foobar", "b"}, + {"food", "e"}, + {"foml", "f"}, + } + keys := make([]string, len(records)) + for i, r := range records { + keys[i] = r.key + } + sort.Strings(keys) + + tree := getTestTree(0) + + // insert all the data + for _, r := range records { + updated, err := tree.Set([]byte(r.key), []byte(r.value)) + require.NoError(t, err) + if updated { + t.Error("should have not been updated") + } + } + // test traversing the whole node works... in order + viewed := []string{} + tree.Iterate(func(key []byte, value []byte) bool { + viewed = append(viewed, string(key)) + return false + }) + if len(viewed) != len(keys) { + t.Error("not the same number of keys as expected") + } + for i, v := range viewed { + if v != keys[i] { + t.Error("Keys out of order", v, keys[i]) + } + } + + trav := traverser{} + tree.IterateRange([]byte("foo"), []byte("goo"), true, trav.view) + expectTraverse(t, trav, "foo", "food", 5) + + trav = traverser{} + tree.IterateRange([]byte("aaa"), []byte("abb"), true, trav.view) + expectTraverse(t, trav, "", "", 0) + + trav = traverser{} + tree.IterateRange(nil, []byte("flap"), true, trav.view) + expectTraverse(t, trav, "abc", "fan", 2) + + trav = traverser{} + tree.IterateRange([]byte("foob"), nil, true, trav.view) + expectTraverse(t, trav, "foobang", "low", 6) + + trav = traverser{} + tree.IterateRange([]byte("very"), nil, true, trav.view) + expectTraverse(t, trav, "", "", 0) + + // make sure it doesn't include end + trav = traverser{} + tree.IterateRange([]byte("fooba"), []byte("food"), true, trav.view) + expectTraverse(t, trav, "foobang", "foobaz", 3) + + // make sure backwards also works... (doesn't include end) + trav = traverser{} + tree.IterateRange([]byte("fooba"), []byte("food"), false, trav.view) + expectTraverse(t, trav, "foobaz", "foobang", 3) + + // make sure backwards also works... + trav = traverser{} + tree.IterateRange([]byte("g"), nil, false, trav.view) + expectTraverse(t, trav, "low", "good", 2) +} + +func TestPersistence(t *testing.T) { + db := dbm.NewMemDB() + + // Create some random key value pairs + records := make(map[string]string) + for i := 0; i < 10000; i++ { + records[randstr(20)] = randstr(20) + } + + // Construct some tree and save it + t1 := NewMutableTree(db, 0, false, log.NewNopLogger()) + for key, value := range records { + t1.Set([]byte(key), []byte(value)) + } + t1.SaveVersion() + + // Load a tree + t2 := NewMutableTree(db, 0, false, log.NewNopLogger()) + t2.Load() + for key, value := range records { + t2value, err := t2.Get([]byte(key)) + require.NoError(t, err) + if string(t2value) != value { + t.Fatalf("Invalid value. Expected %v, got %v", value, t2value) + } + } +} + +func TestProof(t *testing.T) { + // Construct some random tree + tree := getTestTree(100) + for i := 0; i < 10; i++ { + key, value := randstr(20), randstr(20) + tree.Set([]byte(key), []byte(value)) + } + + // Persist the items so far + tree.SaveVersion() + + // Add more items so it's not all persisted + for i := 0; i < 10; i++ { + key, value := randstr(20), randstr(20) + tree.Set([]byte(key), []byte(value)) + } + + // Now for each item, construct a proof and verify + tree.Iterate(func(key []byte, value []byte) bool { + proof, err := tree.GetMembershipProof(key) + assert.NoError(t, err) + assert.Equal(t, value, proof.GetExist().Value) + res, err := tree.VerifyMembership(proof, key) + assert.NoError(t, err) + value2, err := tree.ImmutableTree.Get(key) + assert.NoError(t, err) + if value2 != nil { + assert.True(t, res) + } else { + assert.False(t, res) + } + return false + }) +} + +func TestTreeProof(t *testing.T) { + db := dbm.NewMemDB() + tree := NewMutableTree(db, 100, false, log.NewNopLogger()) + hash := tree.Hash() + assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + + // should get false for proof with nil root + _, err := tree.GetProof([]byte("foo")) + require.Error(t, err) + + // insert lots of info and store the bytes + keys := make([][]byte, 200) + for i := 0; i < 200; i++ { + key := randstr(20) + tree.Set([]byte(key), []byte(key)) + keys[i] = []byte(key) + } + + tree.SaveVersion() + + // query random key fails + _, err = tree.GetMembershipProof([]byte("foo")) + assert.Error(t, err) + + // valid proof for real keys + for _, key := range keys { + proof, err := tree.GetMembershipProof(key) + if assert.NoError(t, err) { + require.Nil(t, err, "Failed to read proof from bytes: %v", err) + assert.Equal(t, key, proof.GetExist().Value) + res, err := tree.VerifyMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } + } +} diff --git a/iavl/batch.go b/iavl/batch.go new file mode 100644 index 000000000..3de9b1684 --- /dev/null +++ b/iavl/batch.go @@ -0,0 +1,128 @@ +package iavl + +import ( + "sync" + + dbm "github.com/cosmos/iavl/db" +) + +// BatchWithFlusher is a wrapper +// around batch that flushes batch's data to disk +// as soon as the configurable limit is reached. +type BatchWithFlusher struct { + mtx sync.Mutex + db dbm.DB // This is only used to create new batch + batch dbm.Batch // Batched writing buffer. + + flushThreshold int // The threshold to flush the batch to disk. +} + +var _ dbm.Batch = (*BatchWithFlusher)(nil) + +// NewBatchWithFlusher returns new BatchWithFlusher wrapping the passed in batch +func NewBatchWithFlusher(db dbm.DB, flushThreshold int) *BatchWithFlusher { + return &BatchWithFlusher{ + db: db, + batch: db.NewBatchWithSize(flushThreshold), + flushThreshold: flushThreshold, + } +} + +// estimateSizeAfterSetting estimates the batch's size after setting a key / value +func (b *BatchWithFlusher) estimateSizeAfterSetting(key []byte, value []byte) (int, error) { + currentSize, err := b.batch.GetByteSize() + if err != nil { + return 0, err + } + // for some batch implementation, when adding a key / value, + // the batch size could gain more than the total size of key and value, + // https://github.com/syndtr/goleveldb/blob/64ee5596c38af10edb6d93e1327b3ed1739747c7/leveldb/batch.go#L98 + + // we add 100 here just to over-account for that overhead + // since estimateSizeAfterSetting is only used to check if we exceed the threshold when setting a key / value + // this means we only over-account for the last key / value + return currentSize + len(key) + len(value) + 100, nil +} + +// Set sets value at the given key to the db. +// If the set causes the underlying batch size to exceed flushThreshold, +// the batch is flushed to disk, cleared, and a new one is created with buffer pre-allocated to threshold. +// The addition entry is then added to the batch. +func (b *BatchWithFlusher) Set(key, value []byte) error { + b.mtx.Lock() + defer b.mtx.Unlock() + + batchSizeAfter, err := b.estimateSizeAfterSetting(key, value) + if err != nil { + return err + } + if batchSizeAfter > b.flushThreshold { + b.mtx.Unlock() + if err := b.Write(); err != nil { + return err + } + b.mtx.Lock() + } + return b.batch.Set(key, value) +} + +// Delete delete value at the given key to the db. +// If the deletion causes the underlying batch size to exceed batchSizeFlushThreshold, +// the batch is flushed to disk, cleared, and a new one is created with buffer pre-allocated to threshold. +// The deletion entry is then added to the batch. +func (b *BatchWithFlusher) Delete(key []byte) error { + b.mtx.Lock() + defer b.mtx.Unlock() + + batchSizeAfter, err := b.estimateSizeAfterSetting(key, []byte{}) + if err != nil { + return err + } + if batchSizeAfter > b.flushThreshold { + b.mtx.Unlock() + if err := b.Write(); err != nil { + return err + } + b.mtx.Lock() + } + return b.batch.Delete(key) +} + +func (b *BatchWithFlusher) Write() error { + b.mtx.Lock() + defer b.mtx.Unlock() + + if err := b.batch.Write(); err != nil { + return err + } + if err := b.batch.Close(); err != nil { + return err + } + b.batch = b.db.NewBatchWithSize(b.flushThreshold) + return nil +} + +func (b *BatchWithFlusher) WriteSync() error { + b.mtx.Lock() + defer b.mtx.Unlock() + + if err := b.batch.WriteSync(); err != nil { + return err + } + if err := b.batch.Close(); err != nil { + return err + } + b.batch = b.db.NewBatchWithSize(b.flushThreshold) + return nil +} + +func (b *BatchWithFlusher) Close() error { + b.mtx.Lock() + defer b.mtx.Unlock() + + return b.batch.Close() +} + +func (b *BatchWithFlusher) GetByteSize() (int, error) { + return b.batch.GetByteSize() +} diff --git a/iavl/batch_test.go b/iavl/batch_test.go new file mode 100644 index 000000000..d89d919e5 --- /dev/null +++ b/iavl/batch_test.go @@ -0,0 +1,71 @@ +package iavl + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +func cleanupDBDir(dir, name string) { + err := os.RemoveAll(filepath.Join(dir, name) + ".db") + if err != nil { + panic(err) + } +} + +var bytesArrayOfSize10KB = [10000]byte{} + +func makeKey(n uint16) []byte { + key := make([]byte, 2) + binary.BigEndian.PutUint16(key, n) + return key +} + +func TestBatchWithFlusher(t *testing.T) { + testedBackends := []string{ + "goleveldb", + } + + for _, backend := range testedBackends { + testBatchWithFlusher(t, backend) + } +} + +func testBatchWithFlusher(t *testing.T, backend string) { + name := fmt.Sprintf("test_%x", randstr(12)) + dir := t.TempDir() + db, err := dbm.NewDB(name, backend, dir) + require.NoError(t, err) + defer cleanupDBDir(dir, name) + + batchWithFlusher := NewBatchWithFlusher(db, DefaultOptions().FlushThreshold) + + // we'll try to to commit 10MBs (1000 * 10KBs each entries) of data into the db + for keyNonce := uint16(0); keyNonce < 1000; keyNonce++ { + // each value is 10 KBs of zero bytes + key := makeKey(keyNonce) + err := batchWithFlusher.Set(key, bytesArrayOfSize10KB[:]) + if err != nil { + panic(err) + } + } + require.NoError(t, batchWithFlusher.Write()) + + itr, err := db.Iterator(nil, nil) + require.NoError(t, err) + + var keyNonce uint16 + for itr.Valid() { + expectedKey := makeKey(keyNonce) + require.Equal(t, expectedKey, itr.Key()) + require.Equal(t, bytesArrayOfSize10KB[:], itr.Value()) + itr.Next() + keyNonce++ + } +} diff --git a/iavl/benchmarks/README.md b/iavl/benchmarks/README.md new file mode 100644 index 000000000..a65990542 --- /dev/null +++ b/iavl/benchmarks/README.md @@ -0,0 +1,61 @@ +# Running Benchmarks + +You should run the benchmarks in a container that has all needed support code, one of those is: + +``` +ghcr.io/notional-labs/cosmos +``` + +and the source for it is here: https://github.com/notional-labs/containers/blob/master/cosmos/Dockerfile + +In + + +## Setting up the machine + +Put the files on the machine and login (all code assumes you are in this directory locally) + +``` +scp -r setup user@host: +ssh user@host +``` + +Run the install script (once per machine) + +``` +cd setup +chmod +x * +sudo ./INSTALL_ROOT.sh +``` + +## Running the tests + +Run the benchmarks in a screen: + +``` +screen +./RUN_BENCHMARKS.sh +``` + +Copy them back from your local machine: + +``` +scp user@host:go/src/github.com/cosmos/iavl/results.txt results.txt +git add results +``` + +## Running benchmarks with docker + +Run the command below to install leveldb and rocksdb from source then run the benchmarks all the dbs (memdb, goleveldb, rocksdb, badgerdb) except boltdb. + +replace: +- `baabeetaa` with your repo username and +- `fix-bencharks` with your branch. + +``` +docker run --rm -it ubuntu:16.04 /bin/bash -c \ +"apt-get update && apt-get install -y curl && \ +sh <(curl -s https://raw.githubusercontent.com/baabeetaa/iavl/fix-bencharks/benchmarks/setup/INSTALL_ROOT.sh) && \ +sh <(curl -s https://raw.githubusercontent.com/baabeetaa/iavl/fix-bencharks/benchmarks/setup/RUN_BENCHMARKS.sh) fix-bencharks baabeetaa && \ +cat ~/iavl/results.txt" +``` diff --git a/iavl/benchmarks/bench_test.go b/iavl/benchmarks/bench_test.go new file mode 100644 index 000000000..8a82590d6 --- /dev/null +++ b/iavl/benchmarks/bench_test.go @@ -0,0 +1,444 @@ +package benchmarks + +import ( + "crypto/rand" + "fmt" + mrand "math/rand" + "os" + "runtime" + "strings" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + "github.com/cosmos/iavl" + dbm "github.com/cosmos/iavl/db" +) + +const historySize = 20 + +func randBytes(length int) []byte { + key := make([]byte, length) + // math.rand.Read always returns err=nil + // we do not need cryptographic randomness for this test: + rand.Read(key) //nolint:errcheck + return key +} + +func prepareTree(b *testing.B, db dbm.DB, size, keyLen, dataLen int) (*iavl.MutableTree, [][]byte) { + t := iavl.NewMutableTree(db, size, false, log.NewNopLogger()) + keys := make([][]byte, size) + + for i := 0; i < size; i++ { + key := randBytes(keyLen) + _, err := t.Set(key, randBytes(dataLen)) + require.NoError(b, err) + keys[i] = key + } + commitTree(b, t) + runtime.GC() + return t, keys +} + +// commit tree saves a new version and deletes old ones according to historySize +func commitTree(b *testing.B, t *iavl.MutableTree) { + t.Hash() + + _, version, err := t.SaveVersion() + if err != nil { + b.Errorf("Can't save: %v", err) + } + + if version > historySize { + err = t.DeleteVersionsTo(version - historySize) + if err != nil { + b.Errorf("Can't delete: %v", err) + } + } +} + +// queries random keys against live state. Keys are almost certainly not in the tree. +func runQueriesFast(b *testing.B, t *iavl.MutableTree, keyLen int) { + isFastCacheEnabled, err := t.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + for i := 0; i < b.N; i++ { + q := randBytes(keyLen) + _, err := t.Get(q) + require.NoError(b, err) + } +} + +// queries keys that are known to be in state +func runKnownQueriesFast(b *testing.B, t *iavl.MutableTree, keys [][]byte) { + isFastCacheEnabled, err := t.IsFastCacheEnabled() // to ensure fast storage is enabled + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + l := int32(len(keys)) + for i := 0; i < b.N; i++ { + q := keys[mrand.Int31n(l)] + _, err := t.Get(q) + require.NoError(b, err) + } +} + +func runQueriesSlow(b *testing.B, t *iavl.MutableTree, keyLen int) { + b.StopTimer() + // Save version to get an old immutable tree to query against, + // Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior. + _, version, err := t.SaveVersion() + require.NoError(b, err) + + itree, err := t.GetImmutable(version - 1) + require.NoError(b, err) + isFastCacheEnabled, err := itree.IsFastCacheEnabled() // to ensure fast storage is enabled + require.NoError(b, err) + require.False(b, isFastCacheEnabled) // to ensure fast storage is not enabled + + b.StartTimer() + for i := 0; i < b.N; i++ { + q := randBytes(keyLen) + _, _, err := itree.GetWithIndex(q) + require.NoError(b, err) + } +} + +func runKnownQueriesSlow(b *testing.B, t *iavl.MutableTree, keys [][]byte) { + b.StopTimer() + // Save version to get an old immutable tree to query against, + // Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior. + _, version, err := t.SaveVersion() + require.NoError(b, err) + + itree, err := t.GetImmutable(version - 1) + require.NoError(b, err) + isFastCacheEnabled, err := itree.IsFastCacheEnabled() // to ensure fast storage is not enabled + require.NoError(b, err) + require.False(b, isFastCacheEnabled) + b.StartTimer() + l := int32(len(keys)) + for i := 0; i < b.N; i++ { + q := keys[mrand.Int31n(l)] + index, value, err := itree.GetWithIndex(q) + require.NoError(b, err) + require.True(b, index >= 0, "the index must not be negative") + require.NotNil(b, value, "the value should exist") + } +} + +func runIterationFast(b *testing.B, t *iavl.MutableTree, expectedSize int) { + isFastCacheEnabled, err := t.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) // to ensure fast storage is enabled + for i := 0; i < b.N; i++ { + itr, err := t.ImmutableTree.Iterator(nil, nil, false) + require.NoError(b, err) + iterate(b, itr, expectedSize) + require.Nil(b, itr.Close(), ".Close should not error out") + } +} + +func runIterationSlow(b *testing.B, t *iavl.MutableTree, expectedSize int) { + for i := 0; i < b.N; i++ { + itr := iavl.NewIterator(nil, nil, false, t.ImmutableTree) // create slow iterator directly + iterate(b, itr, expectedSize) + require.Nil(b, itr.Close(), ".Close should not error out") + } +} + +func iterate(b *testing.B, itr dbm.Iterator, expectedSize int) { + b.StartTimer() + keyValuePairs := make([][][]byte, 0, expectedSize) + for i := 0; i < expectedSize && itr.Valid(); i++ { + itr.Next() + keyValuePairs = append(keyValuePairs, [][]byte{itr.Key(), itr.Value()}) + } + b.StopTimer() + if g, w := len(keyValuePairs), expectedSize; g != w { + b.Errorf("iteration count mismatch: got=%d, want=%d", g, w) + } else if testing.Verbose() { + b.Logf("completed %d iterations", len(keyValuePairs)) + } +} + +// func runInsert(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int) *iavl.MutableTree { +// for i := 1; i <= b.N; i++ { +// t.Set(randBytes(keyLen), randBytes(dataLen)) +// if i%blockSize == 0 { +// t.Hash() +// t.SaveVersion() +// } +// } +// return t +// } + +func runUpdate(b *testing.B, t *iavl.MutableTree, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { + l := int32(len(keys)) + for i := 1; i <= b.N; i++ { + key := keys[mrand.Int31n(l)] + _, err := t.Set(key, randBytes(dataLen)) + require.NoError(b, err) + if i%blockSize == 0 { + commitTree(b, t) + } + } + return t +} + +// func runDelete(b *testing.B, t *iavl.MutableTree, blockSize int, keys [][]byte) *iavl.MutableTree { +// var key []byte +// l := int32(len(keys)) +// for i := 1; i <= b.N; i++ { +// key = keys[rand.Int31n(l)] +// // key = randBytes(16) +// // TODO: test if removed, use more keys (from insert) +// t.Remove(key) +// if i%blockSize == 0 { +// commitTree(b, t) +// } +// } +// return t +// } + +// runBlock measures time for an entire block, not just one tx +func runBlock(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { + l := int32(len(keys)) + + // XXX: This was adapted to work with VersionedTree but needs to be re-thought. + + lastCommit := t + realTree := t + // check := t + + for i := 0; i < b.N; i++ { + for j := 0; j < blockSize; j++ { + // 50% insert, 50% update + var key []byte + if i%2 == 0 { + key = keys[mrand.Int31n(l)] + } else { + key = randBytes(keyLen) + } + data := randBytes(dataLen) + + // perform query and write on check and then realTree + // check.GetFast(key) + // check.Set(key, data) + _, err := realTree.Get(key) + require.NoError(b, err) + _, err = realTree.Set(key, data) + require.NoError(b, err) + } + + // at the end of a block, move it all along.... + commitTree(b, realTree) + lastCommit = realTree + } + + return lastCommit +} + +func BenchmarkRandomBytes(b *testing.B) { + fmt.Printf("%s\n", iavl.GetVersionInfo()) + benchmarks := []struct { + length int + }{ + {4}, {16}, {32}, {100}, {1000}, + } + for _, bench := range benchmarks { + bench := bench + name := fmt.Sprintf("random-%d", bench.length) + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + randBytes(bench.length) + } + runtime.GC() + }) + } +} + +type benchmark struct { + dbType string + initSize, blockSize int + keyLen, dataLen int +} + +func BenchmarkMedium(b *testing.B) { + benchmarks := []benchmark{ + {"memdb", 100000, 100, 16, 40}, + {"goleveldb", 100000, 100, 16, 40}, + {"cleveldb", 100000, 100, 16, 40}, + {"rocksdb", 100000, 100, 16, 40}, + {"pebbledb", 100000, 100, 16, 40}, + } + runBenchmarks(b, benchmarks) +} + +func BenchmarkSmall(b *testing.B) { + benchmarks := []benchmark{ + {"memdb", 1000, 100, 4, 10}, + {"goleveldb", 1000, 100, 4, 10}, + {"cleveldb", 1000, 100, 4, 10}, + {"rocksdb", 1000, 100, 4, 10}, + {"pebbledb", 1000, 100, 4, 10}, + } + runBenchmarks(b, benchmarks) +} + +func BenchmarkLarge(b *testing.B) { + benchmarks := []benchmark{ + {"memdb", 1000000, 100, 16, 40}, + {"goleveldb", 1000000, 100, 16, 40}, + // FIXME: idk why boltdb is too slow !? + // {"boltdb", 1000000, 100, 16, 40}, + // {"rocksdb", 1000000, 100, 16, 40}, + // {"badgerdb", 1000000, 100, 16, 40}, + } + runBenchmarks(b, benchmarks) +} + +func BenchmarkLevelDBBatchSizes(b *testing.B) { + benchmarks := []benchmark{ + {"goleveldb", 100000, 5, 16, 40}, + {"goleveldb", 100000, 25, 16, 40}, + {"goleveldb", 100000, 100, 16, 40}, + {"goleveldb", 100000, 400, 16, 40}, + {"goleveldb", 100000, 2000, 16, 40}, + } + runBenchmarks(b, benchmarks) +} + +// BenchmarkLevelDBLargeData is intended to push disk limits +// in the goleveldb, to make sure not everything is cached +func BenchmarkLevelDBLargeData(b *testing.B) { + benchmarks := []benchmark{ + {"goleveldb", 50000, 100, 32, 100}, + {"goleveldb", 50000, 100, 32, 1000}, + {"goleveldb", 50000, 100, 32, 10000}, + {"goleveldb", 50000, 100, 32, 100000}, + } + runBenchmarks(b, benchmarks) +} + +func runBenchmarks(b *testing.B, benchmarks []benchmark) { + fmt.Printf("%s\n", iavl.GetVersionInfo()) + for _, bb := range benchmarks { + bb := bb + prefix := fmt.Sprintf("%s-%d-%d-%d-%d", bb.dbType, + bb.initSize, bb.blockSize, bb.keyLen, bb.dataLen) + + // prepare a dir for the db and cleanup afterwards + dirName := fmt.Sprintf("./%s-db", prefix) + if bb.dbType == "rocksdb" { + _ = os.Mkdir(dirName, 0o755) + } + + defer func() { + err := os.RemoveAll(dirName) + if err != nil { + b.Errorf("%+v\n", err) + } + }() + + // note that "" leads to nil backing db! + var ( + d dbm.DB + err error + ) + if bb.dbType != "nodb" { + d, err = dbm.NewDB("test", bb.dbType, dirName) + + if err != nil { + if strings.Contains(err.Error(), "unknown db_backend") { + // As an exception to run benchmarks: if the error is about cleveldb, or rocksdb, + // it requires a tag "cleveldb" to link the database at runtime, so instead just + // log the error instead of failing. + b.Logf("%+v\n", err) + continue + } + require.NoError(b, err) + } + defer d.Close() + } + b.Run(prefix, func(sub *testing.B) { + runSuite(sub, d, bb.initSize, bb.blockSize, bb.keyLen, bb.dataLen) + }) + } +} + +// returns number of MB in use +func memUseMB() float64 { + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + asize := mem.Alloc + mb := float64(asize) / 1000000 + return mb +} + +func runSuite(b *testing.B, d dbm.DB, initSize, blockSize, keyLen, dataLen int) { + // measure mem usage + runtime.GC() + init := memUseMB() + + t, keys := prepareTree(b, d, initSize, keyLen, dataLen) + used := memUseMB() - init + fmt.Printf("Init Tree took %0.2f MB\n", used) + + b.ResetTimer() + + b.Run("query-no-in-tree-guarantee-fast", func(sub *testing.B) { + sub.ReportAllocs() + runQueriesFast(sub, t, keyLen) + }) + b.Run("query-no-in-tree-guarantee-slow", func(sub *testing.B) { + sub.ReportAllocs() + runQueriesSlow(sub, t, keyLen) + }) + // + b.Run("query-hits-fast", func(sub *testing.B) { + sub.ReportAllocs() + runKnownQueriesFast(sub, t, keys) + }) + b.Run("query-hits-slow", func(sub *testing.B) { + sub.ReportAllocs() + runKnownQueriesSlow(sub, t, keys) + }) + // + // Iterations for BenchmarkLevelDBLargeData timeout bencher in CI so + // we must skip them. + if b.Name() != "BenchmarkLevelDBLargeData" { + b.Run("iteration-fast", func(sub *testing.B) { + sub.ReportAllocs() + runIterationFast(sub, t, initSize) + }) + b.Run("iteration-slow", func(sub *testing.B) { + sub.ReportAllocs() + runIterationSlow(sub, t, initSize) + }) + } + + // + b.Run("update", func(sub *testing.B) { + sub.ReportAllocs() + t = runUpdate(sub, t, dataLen, blockSize, keys) + }) + b.Run("block", func(sub *testing.B) { + sub.ReportAllocs() + t = runBlock(sub, t, keyLen, dataLen, blockSize, keys) + }) + + // both of these edit size of the tree too much + // need to run with their own tree + // t = nil // for gc + // b.Run("insert", func(sub *testing.B) { + // it, _ := prepareTree(d, initSize, keyLen, dataLen) + // sub.ResetTimer() + // runInsert(sub, it, keyLen, dataLen, blockSize) + // }) + // b.Run("delete", func(sub *testing.B) { + // dt, dkeys := prepareTree(d, initSize+sub.N, keyLen, dataLen) + // sub.ResetTimer() + // runDelete(sub, dt, blockSize, dkeys) + // }) +} diff --git a/iavl/benchmarks/cosmos-exim/README.md b/iavl/benchmarks/cosmos-exim/README.md new file mode 100644 index 000000000..2de7b8d4b --- /dev/null +++ b/iavl/benchmarks/cosmos-exim/README.md @@ -0,0 +1,38 @@ +# cosmos-exim + +A small utility to benchmark export/import of Cosmos Hub IAVL stores. These stores can be downloaded e.g. from [chainlayer.io](https://www.chainlayer.io). Example usage: + +```sh +$ go run benchmarks/cosmos-exim/main.go ../cosmoshub-3/data +Exporting cosmoshub database at version 870068 + +acc : 67131 nodes (33566 leaves) in 676ms with size 3 MB +distribution : 66509 nodes (33255 leaves) in 804ms with size 3 MB +evidence : 0 nodes (0 leaves) in 0s with size 0 MB +god : 0 nodes (0 leaves) in 0s with size 0 MB +main : 1 nodes (1 leaves) in 0s with size 0 MB +mint : 1 nodes (1 leaves) in 0s with size 0 MB +params : 59 nodes (30 leaves) in 0s with size 0 MB +slashing : 1128139 nodes (564070 leaves) in 17.423s with size 41 MB +staking : 44573 nodes (22287 leaves) in 433ms with size 3 MB +supply : 1 nodes (1 leaves) in 0s with size 0 MB +upgrade : 0 nodes (0 leaves) in 0s with size 0 MB + +Exported 11 stores with 1306414 nodes (653211 leaves) in 19.336s with size 52 MB + +Importing into new LevelDB stores + +acc : 67131 nodes (33566 leaves) in 259ms with size 3 MB +distribution: 66509 nodes (33255 leaves) in 238ms with size 3 MB +evidence : 0 nodes (0 leaves) in 19ms with size 0 MB +god : 0 nodes (0 leaves) in 40ms with size 0 MB +main : 1 nodes (1 leaves) in 22ms with size 0 MB +mint : 1 nodes (1 leaves) in 26ms with size 0 MB +params : 59 nodes (30 leaves) in 26ms with size 0 MB +slashing : 1128139 nodes (564070 leaves) in 5.213s with size 41 MB +staking : 44573 nodes (22287 leaves) in 173ms with size 3 MB +supply : 1 nodes (1 leaves) in 25ms with size 0 MB +upgrade : 0 nodes (0 leaves) in 26ms with size 0 MB + +Imported 11 stores with 1306414 nodes (653211 leaves) in 6.067s with size 52 MB +``` \ No newline at end of file diff --git a/iavl/benchmarks/cosmos-exim/main.go b/iavl/benchmarks/cosmos-exim/main.go new file mode 100644 index 000000000..408ad08ca --- /dev/null +++ b/iavl/benchmarks/cosmos-exim/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "fmt" + "os" + "time" + + "cosmossdk.io/log" + tmdb "github.com/cosmos/cosmos-db" + + "github.com/cosmos/iavl" + idbm "github.com/cosmos/iavl/db" +) + +// stores is the list of stores in the CosmosHub database +// FIXME would be nice to autodetect this +var stores = []string{ + "acc", + "distribution", + "evidence", + "god", + "main", + "mint", + "params", + "slashing", + "staking", + "supply", + "upgrade", +} + +// Stats track import/export statistics +type Stats struct { + nodes uint64 + leafNodes uint64 + size uint64 + duration time.Duration +} + +func (s *Stats) Add(o Stats) { + s.nodes += o.nodes + s.leafNodes += o.leafNodes + s.size += o.size + s.duration += o.duration +} + +func (s *Stats) AddDurationSince(started time.Time) { + s.duration += time.Since(started) +} + +func (s *Stats) AddNode(node *iavl.ExportNode) { + s.nodes++ + if node.Height == 0 { + s.leafNodes++ + } + s.size += uint64(len(node.Key) + len(node.Value) + 8 + 1) +} + +func (s *Stats) String() string { + return fmt.Sprintf("%v nodes (%v leaves) in %v with size %v MB", + s.nodes, s.leafNodes, s.duration.Round(time.Millisecond), s.size/1024/1024) +} + +// main runs the main program +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %v \n", os.Args[0]) + os.Exit(1) + } + err := run(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) + os.Exit(1) + } +} + +// run runs the command with normal error handling +func run(dbPath string) error { + version, exports, err := runExport(dbPath) + if err != nil { + return err + } + + err = runImport(version, exports) + if err != nil { + return err + } + return nil +} + +// runExport runs an export benchmark and returns a map of store names/export nodes +func runExport(dbPath string) (int64, map[string][]*iavl.ExportNode, error) { + ldb, err := tmdb.NewDB("application", tmdb.GoLevelDBBackend, dbPath) + if err != nil { + return 0, nil, err + } + tree := iavl.NewMutableTree(idbm.NewWrapper(tmdb.NewPrefixDB(ldb, []byte("s/k:main/"))), 0, false, log.NewNopLogger()) + version, err := tree.LoadVersion(0) + if err != nil { + return 0, nil, err + } + fmt.Printf("Exporting cosmoshub database at version %v\n\n", version) + + exports := make(map[string][]*iavl.ExportNode, len(stores)) + + totalStats := Stats{} + for _, name := range stores { + db := tmdb.NewPrefixDB(ldb, []byte("s/k:"+name+"/")) + tree := iavl.NewMutableTree(idbm.NewWrapper(db), 0, false, log.NewNopLogger()) + + stats := Stats{} + export := make([]*iavl.ExportNode, 0, 100000) + + storeVersion, err := tree.LoadVersion(0) + if err != nil { + return 0, nil, err + } + if storeVersion == 0 { + fmt.Printf("%-13v: %v\n", name, stats.String()) + continue + } + + itree, err := tree.GetImmutable(version) + if err != nil { + return 0, nil, err + } + start := time.Now().UTC() + exporter, err := itree.Export() + if err != nil { + return 0, nil, err + } + defer exporter.Close() + for { + node, err := exporter.Next() + if err == iavl.ErrorExportDone { + break + } else if err != nil { + return 0, nil, err + } + export = append(export, node) + stats.AddNode(node) + } + stats.AddDurationSince(start) + fmt.Printf("%-13v: %v\n", name, stats.String()) + totalStats.Add(stats) + exports[name] = export + } + + fmt.Printf("\nExported %v stores with %v\n\n", len(stores), totalStats.String()) + + return version, exports, nil +} + +// runImport runs an import benchmark with nodes exported from runExport() +func runImport(version int64, exports map[string][]*iavl.ExportNode) error { + fmt.Print("Importing into new LevelDB stores\n\n") + + totalStats := Stats{} + + for _, name := range stores { + tempdir, err := os.MkdirTemp("", name) + if err != nil { + return err + } + defer os.RemoveAll(tempdir) + + start := time.Now() + stats := Stats{} + + newDB, err := tmdb.NewDB(name, tmdb.GoLevelDBBackend, tempdir) + if err != nil { + return err + } + newTree := iavl.NewMutableTree(idbm.NewWrapper(newDB), 0, false, log.NewNopLogger()) + importer, err := newTree.Import(version) + if err != nil { + return err + } + defer importer.Close() + for _, node := range exports[name] { + err = importer.Add(node) + if err != nil { + return err + } + stats.AddNode(node) + } + err = importer.Commit() + if err != nil { + return err + } + stats.AddDurationSince(start) + fmt.Printf("%-12v: %v\n", name, stats.String()) + totalStats.Add(stats) + } + + fmt.Printf("\nImported %v stores with %v\n", len(stores), totalStats.String()) + + return nil +} diff --git a/iavl/benchmarks/hash_test.go b/iavl/benchmarks/hash_test.go new file mode 100644 index 000000000..4ed6c527d --- /dev/null +++ b/iavl/benchmarks/hash_test.go @@ -0,0 +1,53 @@ +package benchmarks + +import ( + "crypto" + "fmt" + "hash" + "testing" + + "github.com/cosmos/iavl" + "github.com/stretchr/testify/require" + + _ "crypto/sha256" + + _ "golang.org/x/crypto/ripemd160" // nolint: staticcheck // need to test ripemd160 + _ "golang.org/x/crypto/sha3" +) + +func BenchmarkHash(b *testing.B) { + fmt.Printf("%s\n", iavl.GetVersionInfo()) + hashers := []struct { + name string + size int + hash hash.Hash + }{ + {"ripemd160", 64, crypto.RIPEMD160.New()}, + {"ripemd160", 512, crypto.RIPEMD160.New()}, + {"sha2-256", 64, crypto.SHA256.New()}, + {"sha2-256", 512, crypto.SHA256.New()}, + {"sha3-256", 64, crypto.SHA3_256.New()}, + {"sha3-256", 512, crypto.SHA3_256.New()}, + } + + for _, h := range hashers { + prefix := fmt.Sprintf("%s-%d", h.name, h.size) + hasher := h + b.Run(prefix, func(sub *testing.B) { + benchHasher(sub, hasher.hash, hasher.size) + }) + } +} + +func benchHasher(b *testing.B, hash hash.Hash, size int) { + // create all random bytes before to avoid timing this + inputs := randBytes(b.N + size + 1) + + for i := 0; i < b.N; i++ { + hash.Reset() + // grab a slice of size bytes from random string + _, err := hash.Write(inputs[i : i+size]) + require.NoError(b, err) + hash.Sum(nil) + } +} diff --git a/iavl/buf.gen.yaml b/iavl/buf.gen.yaml new file mode 100644 index 000000000..876f602e3 --- /dev/null +++ b/iavl/buf.gen.yaml @@ -0,0 +1,9 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/cosmos/iavl/proto +plugins: + - plugin: buf.build/protocolbuffers/go + out: proto + opt: paths=source_relative diff --git a/iavl/cache/cache.go b/iavl/cache/cache.go new file mode 100644 index 000000000..88bf19a2e --- /dev/null +++ b/iavl/cache/cache.go @@ -0,0 +1,117 @@ +package cache + +import ( + "container/list" + + ibytes "github.com/cosmos/iavl/internal/bytes" +) + +// Node represents a node eligible for caching. +type Node interface { + GetKey() []byte +} + +// Cache is an in-memory structure to persist nodes for quick access. +// Please see lruCache for more details about why we need a custom +// cache implementation. +type Cache interface { + // Adds node to cache. If full and had to remove the oldest element, + // returns the oldest, otherwise nil. + // CONTRACT: node can never be nil. Otherwise, cache panics. + Add(node Node) Node + + // Returns Node for the key, if exists. nil otherwise. + Get(key []byte) Node + + // Has returns true if node with key exists in cache, false otherwise. + Has(key []byte) bool + + // Remove removes node with key from cache. The removed node is returned. + // if not in cache, return nil. + Remove(key []byte) Node + + // Len returns the cache length. + Len() int +} + +// lruCache is an LRU cache implementation. +// The motivation for using a custom cache implementation is to +// allow for a custom max policy. +// +// Currently, the cache maximum is implemented in terms of the +// number of nodes which is not intuitive to configure. +// Instead, we are planning to add a byte maximum. +// The alternative implementations do not allow for +// customization and the ability to estimate the byte +// size of the cache. +type lruCache struct { + dict map[string]*list.Element // FastNode cache. + maxElementCount int // FastNode the maximum number of nodes in the cache. + ll *list.List // LRU queue of cache elements. Used for deletion. +} + +var _ Cache = (*lruCache)(nil) + +func New(maxElementCount int) Cache { + return &lruCache{ + dict: make(map[string]*list.Element), + maxElementCount: maxElementCount, + ll: list.New(), + } +} + +func (c *lruCache) Add(node Node) Node { + key := string(node.GetKey()) + if e, exists := c.dict[key]; exists { + c.ll.MoveToFront(e) + old := e.Value + e.Value = node + return old.(Node) + } + + elem := c.ll.PushFront(node) + c.dict[key] = elem + + if c.ll.Len() > c.maxElementCount { + oldest := c.ll.Back() + return c.remove(oldest) + } + return nil +} + +func (c *lruCache) Get(key []byte) Node { + if ele, hit := c.dict[string(key)]; hit { + c.ll.MoveToFront(ele) + return ele.Value.(Node) + } + return nil +} + +func (c *lruCache) Has(key []byte) bool { + _, exists := c.dict[string(key)] + return exists +} + +func (c *lruCache) Len() int { + return c.ll.Len() +} + +func (c *lruCache) Remove(key []byte) Node { + keyS := string(key) + if elem, exists := c.dict[keyS]; exists { + return c.removeWithKey(elem, keyS) + } + return nil +} + +func (c *lruCache) remove(e *list.Element) Node { + removed := c.ll.Remove(e).(Node) + delete(c.dict, ibytes.UnsafeBytesToStr(removed.GetKey())) + return removed +} + +func (c *lruCache) removeWithKey(e *list.Element, key string) Node { + removed := c.ll.Remove(e).(Node) + delete(c.dict, key) + return removed +} diff --git a/iavl/cache/cache_bench_test.go b/iavl/cache/cache_bench_test.go new file mode 100644 index 000000000..7332b6869 --- /dev/null +++ b/iavl/cache/cache_bench_test.go @@ -0,0 +1,69 @@ +package cache_test + +import ( + "math/rand" + "testing" + + "github.com/cosmos/iavl/cache" +) + +func BenchmarkAdd(b *testing.B) { + b.ReportAllocs() + testcases := map[string]struct { + cacheMax int + keySize int + }{ + "small - max: 10K, key size - 10b": { + cacheMax: 10000, + keySize: 10, + }, + "med - max: 100K, key size 20b": { + cacheMax: 100000, + keySize: 20, + }, + "large - max: 1M, key size 30b": { + cacheMax: 1000000, + keySize: 30, + }, + } + + for name, tc := range testcases { + cache := cache.New(tc.cacheMax) + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + key := randBytes(tc.keySize) + b.StartTimer() + + _ = cache.Add(&testNode{ + key: key, + }) + } + }) + } +} + +func BenchmarkRemove(b *testing.B) { + b.ReportAllocs() + + cache := cache.New(1000) + existentKeyMirror := [][]byte{} + // Populate cache + for i := 0; i < 50; i++ { + key := randBytes(1000) + + existentKeyMirror = append(existentKeyMirror, key) + + cache.Add(&testNode{ + key: key, + }) + } + + randSeed := 498727689 // For deterministic tests + r := rand.New(rand.NewSource(int64(randSeed))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := existentKeyMirror[r.Intn(len(existentKeyMirror))] + _ = cache.Remove(key) + } +} diff --git a/iavl/cache/cache_test.go b/iavl/cache/cache_test.go new file mode 100644 index 000000000..4a0c75bf4 --- /dev/null +++ b/iavl/cache/cache_test.go @@ -0,0 +1,309 @@ +package cache_test + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/cosmos/iavl/cache" + "github.com/stretchr/testify/require" +) + +// expectedResult represents the expected result of each add/remove operation. +// It can be noneRemoved or the index of the removed node in testNodes +type expectedResult int + +const ( + noneRemoved expectedResult = -1 + // The rest represent the index of the removed node +) + +// testNode is the node used for testing cache implementation +type testNode struct { + key []byte +} + +type cacheOp struct { + testNodexIdx int + expectedResult expectedResult +} + +type testcase struct { + setup func(cache.Cache) + cacheMax int + cacheOps []cacheOp + expectedNodeIndexes []int // contents of the cache once test case completes represent by indexes in testNodes +} + +func (tn *testNode) GetKey() []byte { + return tn.key +} + +const ( + testKey = "key" +) + +var _ cache.Node = (*testNode)(nil) + +var testNodes = []cache.Node{ + &testNode{ + key: []byte(fmt.Sprintf("%s%d", testKey, 1)), + }, + &testNode{ + key: []byte(fmt.Sprintf("%s%d", testKey, 2)), + }, + &testNode{ + key: []byte(fmt.Sprintf("%s%d", testKey, 3)), + }, +} + +func Test_Cache_Add(t *testing.T) { + testcases := map[string]testcase{ + "add 1 node with 1 max - added": { + cacheMax: 1, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + }, + expectedNodeIndexes: []int{0}, + }, + "add 1 node twice, cache max 2 - only one added": { + cacheMax: 2, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 0, + expectedResult: 0, + }, + }, + expectedNodeIndexes: []int{0}, + }, + "add 1 node with 0 max - not added and return itself": { + cacheMax: 0, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: 0, + }, + }, + }, + "add 3 nodes with 1 max - first 2 removed": { + cacheMax: 1, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 1, + expectedResult: 0, + }, + { + testNodexIdx: 2, + expectedResult: 1, + }, + }, + expectedNodeIndexes: []int{2}, + }, + "add 3 nodes with 2 max - first removed": { + cacheMax: 2, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 1, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 2, + expectedResult: 0, + }, + }, + expectedNodeIndexes: []int{1, 2}, + }, + "add 3 nodes with 10 max - non removed": { + cacheMax: 10, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 1, + expectedResult: noneRemoved, + }, + { + testNodexIdx: 2, + expectedResult: noneRemoved, + }, + }, + expectedNodeIndexes: []int{0, 1, 2}, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + cache := cache.New(tc.cacheMax) + + expectedCurSize := 0 + + for _, op := range tc.cacheOps { + + actualResult := cache.Add(testNodes[op.testNodexIdx]) + + expectedResult := op.expectedResult + + if expectedResult == noneRemoved { + require.Nil(t, actualResult) + expectedCurSize++ + } else { + require.NotNil(t, actualResult) + + // Here, op.expectedResult represents the index of the removed node in tc.cacheOps + require.Equal(t, testNodes[int(op.expectedResult)], actualResult) + } + require.Equal(t, expectedCurSize, cache.Len()) + } + + validateCacheContentsAfterTest(t, tc, cache) + }) + } +} + +func Test_Cache_Remove(t *testing.T) { + testcases := map[string]testcase{ + "remove non-existent key, cache max 0 - nil returned": { + cacheMax: 0, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + }, + }, + "remove non-existent key, cache max 1 - nil returned": { + setup: func(c cache.Cache) { + require.Nil(t, c.Add(testNodes[1])) + require.Equal(t, 1, c.Len()) + }, + cacheMax: 1, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + }, + expectedNodeIndexes: []int{1}, + }, + "remove existent key, cache max 1 - removed": { + setup: func(c cache.Cache) { + require.Nil(t, c.Add(testNodes[0])) + require.Equal(t, 1, c.Len()) + }, + cacheMax: 1, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: 0, + }, + }, + }, + "remove twice, cache max 1 - removed first time, then nil": { + setup: func(c cache.Cache) { + require.Nil(t, c.Add(testNodes[0])) + require.Equal(t, 1, c.Len()) + }, + cacheMax: 1, + cacheOps: []cacheOp{ + { + testNodexIdx: 0, + expectedResult: 0, + }, + { + testNodexIdx: 0, + expectedResult: noneRemoved, + }, + }, + }, + "remove all, cache max 3": { + setup: func(c cache.Cache) { + require.Nil(t, c.Add(testNodes[0])) + require.Nil(t, c.Add(testNodes[1])) + require.Nil(t, c.Add(testNodes[2])) + require.Equal(t, 3, c.Len()) + }, + cacheMax: 3, + cacheOps: []cacheOp{ + { + testNodexIdx: 2, + expectedResult: 2, + }, + { + testNodexIdx: 0, + expectedResult: 0, + }, + { + testNodexIdx: 1, + expectedResult: 1, + }, + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + cache := cache.New(tc.cacheMax) + + if tc.setup != nil { + tc.setup(cache) + } + + expectedCurSize := cache.Len() + + for _, op := range tc.cacheOps { + + actualResult := cache.Remove(testNodes[op.testNodexIdx].GetKey()) + + expectedResult := op.expectedResult + + if expectedResult == noneRemoved { + require.Nil(t, actualResult) + } else { + expectedCurSize-- + require.NotNil(t, actualResult) + + // Here, op.expectedResult represents the index of the removed node in tc.cacheOps + require.Equal(t, testNodes[int(op.expectedResult)], actualResult) + } + require.Equal(t, expectedCurSize, cache.Len()) + } + + validateCacheContentsAfterTest(t, tc, cache) + }) + } +} + +func validateCacheContentsAfterTest(t *testing.T, tc testcase, cache cache.Cache) { + require.Equal(t, len(tc.expectedNodeIndexes), cache.Len()) + for _, idx := range tc.expectedNodeIndexes { + expectedNode := testNodes[idx] + require.True(t, cache.Has(expectedNode.GetKey())) + require.Equal(t, expectedNode, cache.Get(expectedNode.GetKey())) + } +} + +func randBytes(length int) []byte { + key := make([]byte, length) + // math.rand.Read always returns err=nil + // we do not need cryptographic randomness for this test: + + rand.Read(key) //nolint:errcheck + return key +} diff --git a/iavl/cmd/iaviewer/README.md b/iavl/cmd/iaviewer/README.md new file mode 100644 index 000000000..ef21f1b98 --- /dev/null +++ b/iavl/cmd/iaviewer/README.md @@ -0,0 +1,118 @@ +# IaViewer + +`iaviewer` is a utility to inspect the contents of a persisted iavl tree, given (a copy of) the leveldb store. +This can be quite useful for debugging, especially when you find odd errors, or non-deterministic behavior. +Below is a brief introduction to the tool. + +## Installation + +Once this is merged into the offical repo, master, you should be able to do: + +```shell +go get github.com/cosmos/iavl +cd ${GOPATH}/src/github.com/cosmos/iavl +make install +``` + +## Using the tool + +First make sure it is properly installed and you have `${GOPATH}/bin` in your `PATH`. +Typing `iaviewer` should run and print out a usage message. + +### Sample databases + +Once you understand the tool, you will most likely want to run it on captures from your +own abci app (built on cosmos-sdk or weave), but as a tutorial, you can try to use some +captures from an actual bug I found in my code... Same data, different hash. + +```shell +mkdir ./testdata +cd ./testdata +curl -L https://github.com/iov-one/iavl/files/2860877/bns-a.db.zip > bns-a.db.zip +unzip bns-a.db.zip +curl -L https://github.com/iov-one/iavl/files/2860878/bns-b.db.zip > bns-b.db.zip +unzip bns-b.db.zip +``` + +Now, if you run `ls -l`, you should see two directories... `bns-a.db` and `bns-b.db` + +### Inspecting available versions + +```shell +iaviewer versions ./bns-a.db "" +``` + +This should print out a list of 20 versions of the code. Note the the iavl tree will persist multiple +historical versions, which is a great aid in forensic queries (thanks Tendermint team!). For the rest +of the cases, we will consider only the last two versions, 190257 (last one where they match) and 190258 +(where they are different). + +### Checking keys and app hash + +First run these two and take a quick a look at the output: + +```shell +iaviewer data ./bns-a.db "" +iaviewer data ./bns-a.db "" 190257 +``` + +Notice you see the different heights and there is a change in size and app hash. +That's what happens when we process a transaction. Let's go further and use +the handy tool `diff` to compare two states. + +```shell +iaviewer data ./bns-a.db "" 190257 > a-last.data +iaviewer data ./bns-b.db "" 190257 > b-last.data + +diff a-last.data b-last.data +``` + +Same, same :) +But if we take the current version... + +```shell +iaviewer data ./bns-a.db "" 190258 > a-cur.data +iaviewer data ./bns-b.db "" 190258 > b-cur.data + +diff a-cur.data b-cur.data +``` + +Hmmm... everything is the same, except the hash. Odd... +So odd that I [wrote an article about it](https://medium.com/@ethan.frey/tracking-down-a-tendermint-consensus-failure-77f6ff414406) + +And finally, if we want to inspect which keys were modified in the last block: + +```shell +diff a-cur.data a-last.data +``` + +You should see 6 writes.. the `_i.usernft_*` are the secondary indexes on the username nft. +`sigs.*` is setting the nonce (if this were an update, you would see a previous value). +And `usrnft:*` is creating the actual username nft. + +### Checking the tree shape + +So, remember above, when we found that the current state of a and b have the same data +but different hashes. This must be due to the shape of the iavl tree. +To confirm that, and possibly get more insights, there is another command. + +```shell +iaviewer shape ./bns-a.db "" 190258 > a-cur.shape +iaviewer shape ./bns-b.db "" 190258 > b-cur.shape + +diff a-cur.shape b-cur.shape +``` + +Yup, that is quite some difference. You can also look at the tree as a whole. +So, stretch your terminal nice and wide, and... + +```shell +less a-cur.shape +``` + +It has `-5 ` for an inner node of depth 5, and `*6 ` for a leaf node (data) of depth 6. +Indentation also suggests the shape of the tree. + +Note, if anyone wants to improve the visualization, that would be awesome. +I have no idea how to do this well, but at least text output makes some +sense and is diff-able. \ No newline at end of file diff --git a/iavl/cmd/iaviewer/main.go b/iavl/cmd/iaviewer/main.go new file mode 100644 index 000000000..796741698 --- /dev/null +++ b/iavl/cmd/iaviewer/main.go @@ -0,0 +1,188 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "cosmossdk.io/log" + dbm "github.com/cosmos/cosmos-db" + + "github.com/cosmos/iavl" + idbm "github.com/cosmos/iavl/db" +) + +// TODO: make this configurable? +const ( + DefaultCacheSize int = 10000 +) + +func main() { + args := os.Args[1:] + if len(args) < 3 || (args[0] != "data" && args[0] != "shape" && args[0] != "versions") { + fmt.Fprintln(os.Stderr, "Usage: iaviewer [version number]") + fmt.Fprintln(os.Stderr, " is the prefix of db, and the iavl tree of different modules in cosmos-sdk uses ") + fmt.Fprintln(os.Stderr, "different to identify, just like \"s/k:gov/\" represents the prefix of gov module") + os.Exit(1) + } + + version := 0 + if len(args) == 4 { + var err error + version, err = strconv.Atoi(args[3]) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid version number: %s\n", err) + os.Exit(1) + } + } + + tree, err := ReadTree(args[1], version, []byte(args[2])) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading data: %s\n", err) + os.Exit(1) + } + + switch args[0] { + case "data": + PrintKeys(tree) + hash := tree.Hash() + fmt.Printf("Hash: %X\n", hash) + fmt.Printf("Size: %X\n", tree.Size()) + case "shape": + PrintShape(tree) + case "versions": + PrintVersions(tree) + } +} + +func OpenDB(dir string) (dbm.DB, error) { + switch { + case strings.HasSuffix(dir, ".db"): + dir = dir[:len(dir)-3] + case strings.HasSuffix(dir, ".db/"): + dir = dir[:len(dir)-4] + default: + return nil, fmt.Errorf("database directory must end with .db") + } + + dir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + // TODO: doesn't work on windows! + cut := strings.LastIndex(dir, "/") + if cut == -1 { + return nil, fmt.Errorf("cannot cut paths on %s", dir) + } + name := dir[cut+1:] + db, err := dbm.NewGoLevelDB(name, dir[:cut], nil) + if err != nil { + return nil, err + } + return db, nil +} + +func PrintDBStats(db dbm.DB) { + count := 0 + prefix := map[string]int{} + itr, err := db.Iterator(nil, nil) + if err != nil { + panic(err) + } + + defer itr.Close() + for ; itr.Valid(); itr.Next() { + key := itr.Key()[:1] + prefix[string(key)]++ + count++ + } + if err := itr.Error(); err != nil { + panic(err) + } + fmt.Printf("DB contains %d entries\n", count) + for k, v := range prefix { + fmt.Printf(" %s: %d\n", k, v) + } +} + +// ReadTree loads an iavl tree from the directory +// If version is 0, load latest, otherwise, load named version +// The prefix represents which iavl tree you want to read. The iaviwer will always set a prefix. +func ReadTree(dir string, version int, prefix []byte) (*iavl.MutableTree, error) { + db, err := OpenDB(dir) + if err != nil { + return nil, err + } + if len(prefix) != 0 { + db = dbm.NewPrefixDB(db, prefix) + } + + tree := iavl.NewMutableTree(idbm.NewWrapper(db), DefaultCacheSize, false, log.NewLogger(os.Stdout)) + ver, err := tree.LoadVersion(int64(version)) + fmt.Printf("Got version: %d\n", ver) + return tree, err +} + +func PrintKeys(tree *iavl.MutableTree) { + fmt.Println("Printing all keys with hashed values (to detect diff)") + tree.Iterate(func(key []byte, value []byte) bool { //nolint:errcheck + printKey := parseWeaveKey(key) + digest := sha256.Sum256(value) + fmt.Printf(" %s\n %X\n", printKey, digest) + return false + }) +} + +// parseWeaveKey assumes a separating : where all in front should be ascii, +// and all afterwards may be ascii or binary +func parseWeaveKey(key []byte) string { + cut := bytes.IndexRune(key, ':') + if cut == -1 { + return encodeID(key) + } + prefix := key[:cut] + id := key[cut+1:] + return fmt.Sprintf("%s:%s", encodeID(prefix), encodeID(id)) +} + +// casts to a string if it is printable ascii, hex-encodes otherwise +func encodeID(id []byte) string { + for _, b := range id { + if b < 0x20 || b >= 0x80 { + return strings.ToUpper(hex.EncodeToString(id)) + } + } + return string(id) +} + +func PrintShape(tree *iavl.MutableTree) { + // shape := tree.RenderShape(" ", nil) + // TODO: handle this error + shape, _ := tree.RenderShape(" ", nodeEncoder) + fmt.Println(strings.Join(shape, "\n")) +} + +func nodeEncoder(id []byte, depth int, isLeaf bool) string { + prefix := fmt.Sprintf("-%d ", depth) + if isLeaf { + prefix = fmt.Sprintf("*%d ", depth) + } + if len(id) == 0 { + return fmt.Sprintf("%s", prefix) + } + return fmt.Sprintf("%s%s", prefix, parseWeaveKey(id)) +} + +func PrintVersions(tree *iavl.MutableTree) { + versions := tree.AvailableVersions() + fmt.Println("Available versions:") + for _, v := range versions { + fmt.Printf(" %d\n", v) + } +} diff --git a/iavl/cmd/legacydump/README.md b/iavl/cmd/legacydump/README.md new file mode 100644 index 000000000..983b3882d --- /dev/null +++ b/iavl/cmd/legacydump/README.md @@ -0,0 +1,18 @@ +# LegacyDump + +`legacydump` is a command line tool to generate a `iavl` tree based on the legacy format of the node key. +This tool is used for testing the `lazy loading and set` feature of the `iavl` tree. + +## Usage + +It takes 5 arguments: + + - dbtype: the type of database to use. + - dbdir: the directory to store the database. + - `random` or `sequential`: The `sequential` option will generate the tree from `1` to `version` in order and delete versions from `1` to `removal version`. The `random` option will delete `removal version` versions randomly. + - version: the upto number of versions to generate. + - removal version: the number of versions to remove. + +```shell +go run . +``` diff --git a/iavl/cmd/legacydump/go.mod b/iavl/cmd/legacydump/go.mod new file mode 100644 index 000000000..4631e0ce2 --- /dev/null +++ b/iavl/cmd/legacydump/go.mod @@ -0,0 +1,31 @@ +module github.com/cosmos/iavl/cmd/legacydump + +go 1.20 + +require ( + github.com/cometbft/cometbft-db v0.7.0 + github.com/cosmos/iavl v0.20.0 +) + +require ( + github.com/cespare/xxhash v1.1.0 // indirect + github.com/confio/ics23/go v0.9.0 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect + github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca // indirect + github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + go.etcd.io/bbolt v1.3.6 // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) diff --git a/iavl/cmd/legacydump/go.sum b/iavl/cmd/legacydump/go.sum new file mode 100644 index 000000000..b9ef6ad67 --- /dev/null +++ b/iavl/cmd/legacydump/go.sum @@ -0,0 +1,182 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cometbft/cometbft-db v0.7.0 h1:uBjbrBx4QzU0zOEnU8KxoDl18dMNgDh+zZRUE0ucsbo= +github.com/cometbft/cometbft-db v0.7.0/go.mod h1:yiKJIm2WKrt6x8Cyxtq9YTEcIMPcEe4XPxhgX59Fzf0= +github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= +github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosmos/iavl v0.20.0 h1:fTVznVlepH0KK8NyKq8w+U7c2L6jofa27aFX6YGlm38= +github.com/cosmos/iavl v0.20.0/go.mod h1:WO7FyvaZJoH65+HFOsDir7xU9FWk2w9cHXNW1XHcl7A= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +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 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +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 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +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/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk= +github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-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-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +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-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/iavl/cmd/legacydump/legacydump b/iavl/cmd/legacydump/legacydump new file mode 100755 index 000000000..9992570af Binary files /dev/null and b/iavl/cmd/legacydump/legacydump differ diff --git a/iavl/cmd/legacydump/main.go b/iavl/cmd/legacydump/main.go new file mode 100644 index 000000000..72345e7cc --- /dev/null +++ b/iavl/cmd/legacydump/main.go @@ -0,0 +1,125 @@ +package main + +import ( + cryptorand "crypto/rand" + "fmt" + "math/rand" + "os" + "path/filepath" + "strconv" + + dbm "github.com/cometbft/cometbft-db" + "github.com/cosmos/iavl" +) + +const ( + DefaultCacheSize = 1000 +) + +func main() { + args := os.Args[1:] + if len(args) < 5 { + fmt.Fprintln(os.Stderr, "Usage: legacydump ") + os.Exit(1) + } + + version, err := strconv.Atoi(args[3]) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid version number: %s\n", err) + os.Exit(1) + } + + removalVersion, err := strconv.Atoi(args[4]) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid removal version number: %s\n", err) + } + + if err = GenerateTree(args[0], args[1], args[2], version, removalVersion); err != nil { + fmt.Fprintf(os.Stderr, "Error generating tree: %s\n", err) + } +} + +func openDB(dbType, dbDir string) (dbm.DB, error) { + dir, err := filepath.Abs(dbDir) + if err != nil { + return nil, err + } + + db, err := dbm.NewDB("test", dbm.BackendType(dbType), dir) + if err != nil { + return nil, err + } + return db, nil +} + +// GenerateTree generates a tree with the given number of versions. +func GenerateTree(dbType, dbDir, mode string, version, removalVersion int) error { + db, err := openDB(dbType, dbDir) + if err != nil { + return err + } + defer db.Close() + + switch mode { + case "random": + return generateRandomTree(db, version, removalVersion) + case "sequential": + _, err = generateSequentialTree(db, version, removalVersion) + return err + default: + return fmt.Errorf("invalid mode: %s", mode) + } +} + +func generateRandomTree(db dbm.DB, version, removalVersion int) error { + t, err := generateSequentialTree(db, version, 0) + if err != nil { + return err + } + + // delete the versions + versions := make([]int64, version) + for i := 0; i < version; i++ { + versions[i] = int64(i + 1) + } + + // make sure the latest version is not deleted + for i := 1; i <= removalVersion; i++ { + index := rand.Intn(version - i) + if err := t.DeleteVersion(versions[index]); err != nil { + return err + } + versions[index], versions[version-i-1] = versions[version-i-1], versions[index] + } + + return nil +} + +func generateSequentialTree(db dbm.DB, version, removalVersion int) (*iavl.MutableTree, error) { + t, err := iavl.NewMutableTreeWithOpts(db, DefaultCacheSize, nil, false) + if err != nil { + return nil, err + } + + for i := 0; i < version; i++ { + leafCount := rand.Int31n(50) + for j := int32(0); j < leafCount; j++ { + t.Set(randBytes(32), randBytes(32)) + } + if _, _, err = t.SaveVersion(); err != nil { + return nil, err + } + } + + if removalVersion > 0 { + err = t.DeleteVersionsRange(1, int64(removalVersion)+1) + } + + return t, err +} + +func randBytes(length int) []byte { + key := make([]byte, length) + _, _ = cryptorand.Read(key) + return key +} diff --git a/iavl/compress.go b/iavl/compress.go new file mode 100644 index 000000000..0e41150fc --- /dev/null +++ b/iavl/compress.go @@ -0,0 +1,144 @@ +package iavl + +import ( + "encoding/binary" + "fmt" +) + +type NodeExporter interface { + Next() (*ExportNode, error) +} + +type NodeImporter interface { + Add(*ExportNode) error +} + +// CompressExporter wraps the normal exporter to apply some compressions on `ExportNode`: +// - branch keys are skipped +// - leaf keys are encoded with delta compared with the previous leaf +// - branch node's version are encoded with delta compared with the max version in it's children +type CompressExporter struct { + inner NodeExporter + lastKey []byte + versionStack []int64 +} + +var _ NodeExporter = (*CompressExporter)(nil) + +func NewCompressExporter(exporter NodeExporter) NodeExporter { + return &CompressExporter{inner: exporter} +} + +func (e *CompressExporter) Next() (*ExportNode, error) { + n, err := e.inner.Next() + if err != nil { + return nil, err + } + + if n.Height == 0 { + // apply delta encoding to leaf keys + n.Key, e.lastKey = deltaEncode(n.Key, e.lastKey), n.Key + + e.versionStack = append(e.versionStack, n.Version) + } else { + // branch keys can be derived on the fly when import, safe to skip + n.Key = nil + + // delta encode the version + maxVersion := maxInt64(e.versionStack[len(e.versionStack)-1], e.versionStack[len(e.versionStack)-2]) + e.versionStack = e.versionStack[:len(e.versionStack)-1] + e.versionStack[len(e.versionStack)-1] = n.Version + n.Version -= maxVersion + } + + return n, nil +} + +// CompressImporter wraps the normal importer to do de-compressions before hand. +type CompressImporter struct { + inner NodeImporter + lastKey []byte + minKeyStack [][]byte + versionStack []int64 +} + +var _ NodeImporter = (*CompressImporter)(nil) + +func NewCompressImporter(importer NodeImporter) NodeImporter { + return &CompressImporter{inner: importer} +} + +func (i *CompressImporter) Add(node *ExportNode) error { + if node.Height == 0 { + key, err := deltaDecode(node.Key, i.lastKey) + if err != nil { + return err + } + node.Key = key + i.lastKey = key + + i.minKeyStack = append(i.minKeyStack, key) + i.versionStack = append(i.versionStack, node.Version) + } else { + // use the min-key in right branch as the node key + node.Key = i.minKeyStack[len(i.minKeyStack)-1] + // leave the min-key in left branch in the stack + i.minKeyStack = i.minKeyStack[:len(i.minKeyStack)-1] + + // decode branch node version + maxVersion := maxInt64(i.versionStack[len(i.versionStack)-1], i.versionStack[len(i.versionStack)-2]) + node.Version += maxVersion + i.versionStack = i.versionStack[:len(i.versionStack)-1] + i.versionStack[len(i.versionStack)-1] = node.Version + } + + return i.inner.Add(node) +} + +func deltaEncode(key, lastKey []byte) []byte { + var sizeBuf [binary.MaxVarintLen64]byte + shared := diffOffset(lastKey, key) + n := binary.PutUvarint(sizeBuf[:], uint64(shared)) + return append(sizeBuf[:n], key[shared:]...) +} + +func deltaDecode(key, lastKey []byte) ([]byte, error) { + shared, n := binary.Uvarint(key) + if n <= 0 { + return nil, fmt.Errorf("uvarint parse failed %d", n) + } + + key = key[n:] + if shared == 0 { + return key, nil + } + + newKey := make([]byte, shared+uint64(len(key))) + copy(newKey, lastKey[:shared]) + copy(newKey[shared:], key) + return newKey, nil +} + +// diffOffset returns the index of first byte that's different in two bytes slice. +func diffOffset(a, b []byte) int { + var off int + var l int + if len(a) < len(b) { + l = len(a) + } else { + l = len(b) + } + for ; off < l; off++ { + if a[off] != b[off] { + break + } + } + return off +} + +func maxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} diff --git a/iavl/db/memdb.go b/iavl/db/memdb.go new file mode 100644 index 000000000..614c52f94 --- /dev/null +++ b/iavl/db/memdb.go @@ -0,0 +1,462 @@ +package db + +import ( + "bytes" + "context" + "fmt" + "sync" + + "github.com/google/btree" +) + +const ( + // The approximate number of items and children per B-tree node. Tuned with benchmarks. + bTreeDegree = 32 +) + +// item is a btree.Item with byte slices as keys and values +type item struct { + key []byte + value []byte +} + +// Less implements btree.Item. +func (i item) Less(other btree.Item) bool { + // this considers nil == []byte{}, but that's ok since we handle nil endpoints + // in iterators specially anyway + return bytes.Compare(i.key, other.(item).key) == -1 +} + +// newKey creates a new key item. +func newKey(key []byte) item { + return item{key: key} +} + +// newPair creates a new pair item. +func newPair(key, value []byte) item { + return item{key: key, value: value} +} + +// MemDB is an in-memory database backend using a B-tree for the test purpose. +// +// For performance reasons, all given and returned keys and values are pointers to the in-memory +// database, so modifying them will cause the stored values to be modified as well. All DB methods +// already specify that keys and values should be considered read-only, but this is especially +// important with MemDB. +type MemDB struct { + mtx sync.RWMutex + btree *btree.BTree +} + +var _ DB = (*MemDB)(nil) + +// NewMemDB creates a new in-memory database. +func NewMemDB() *MemDB { + database := &MemDB{ + btree: btree.New(bTreeDegree), + } + return database +} + +// Get implements DB. +func (db *MemDB) Get(key []byte) ([]byte, error) { + if len(key) == 0 { + return nil, errKeyEmpty + } + db.mtx.RLock() + defer db.mtx.RUnlock() + + i := db.btree.Get(newKey(key)) + if i != nil { + return i.(item).value, nil + } + return nil, nil +} + +// Has implements DB. +func (db *MemDB) Has(key []byte) (bool, error) { + if len(key) == 0 { + return false, errKeyEmpty + } + db.mtx.RLock() + defer db.mtx.RUnlock() + + return db.btree.Has(newKey(key)), nil +} + +// Set implements DB. +func (db *MemDB) Set(key []byte, value []byte) error { + if len(key) == 0 { + return errKeyEmpty + } + if value == nil { + return errValueNil + } + db.mtx.Lock() + defer db.mtx.Unlock() + + db.set(key, value) + return nil +} + +// set sets a value without locking the mutex. +func (db *MemDB) set(key []byte, value []byte) { + db.btree.ReplaceOrInsert(newPair(key, value)) +} + +// SetSync implements DB. +func (db *MemDB) SetSync(key []byte, value []byte) error { + return db.Set(key, value) +} + +// Delete implements DB. +func (db *MemDB) Delete(key []byte) error { + if len(key) == 0 { + return errKeyEmpty + } + db.mtx.Lock() + defer db.mtx.Unlock() + + db.delete(key) + return nil +} + +// delete deletes a key without locking the mutex. +func (db *MemDB) delete(key []byte) { + db.btree.Delete(newKey(key)) +} + +// DeleteSync implements DB. +func (db *MemDB) DeleteSync(key []byte) error { + return db.Delete(key) +} + +// Close implements DB. +func (db *MemDB) Close() error { + // Close is a noop since for an in-memory database, we don't have a destination to flush + // contents to nor do we want any data loss on invoking Close(). + // See the discussion in https://github.com/tendermint/tendermint/libs/pull/56 + return nil +} + +// Print implements DB. +func (db *MemDB) Print() error { + db.mtx.RLock() + defer db.mtx.RUnlock() + + db.btree.Ascend(func(i btree.Item) bool { + item := i.(item) + fmt.Printf("[%X]:\t[%X]\n", item.key, item.value) + return true + }) + return nil +} + +// Stats implements DB. +func (db *MemDB) Stats() map[string]string { + db.mtx.RLock() + defer db.mtx.RUnlock() + + stats := make(map[string]string) + stats["database.type"] = "memDB" + stats["database.size"] = fmt.Sprintf("%d", db.btree.Len()) + return stats +} + +// NewBatch implements DB. +func (db *MemDB) NewBatch() Batch { + return newMemDBBatch(db) +} + +// NewBatchWithSize implements DB. +// It does the same thing as NewBatch because we can't pre-allocate memDBBatch +func (db *MemDB) NewBatchWithSize(_ int) Batch { + return newMemDBBatch(db) +} + +// Iterator implements DB. +// Takes out a read-lock on the database until the iterator is closed. +func (db *MemDB) Iterator(start, end []byte) (Iterator, error) { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + return nil, errKeyEmpty + } + return newMemDBIterator(db, start, end, false), nil +} + +// ReverseIterator implements DB. +// Takes out a read-lock on the database until the iterator is closed. +func (db *MemDB) ReverseIterator(start, end []byte) (Iterator, error) { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + return nil, errKeyEmpty + } + return newMemDBIterator(db, start, end, true), nil +} + +// IteratorNoMtx makes an iterator with no mutex. +func (db *MemDB) IteratorNoMtx(start, end []byte) (Iterator, error) { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + return nil, errKeyEmpty + } + return newMemDBIteratorMtxChoice(db, start, end, false, false), nil +} + +// ReverseIteratorNoMtx makes an iterator with no mutex. +func (db *MemDB) ReverseIteratorNoMtx(start, end []byte) (Iterator, error) { + if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { + return nil, errKeyEmpty + } + return newMemDBIteratorMtxChoice(db, start, end, true, false), nil +} + +const ( + // Size of the channel buffer between traversal goroutine and iterator. Using an unbuffered + // channel causes two context switches per item sent, while buffering allows more work per + // context switch. Tuned with benchmarks. + chBufferSize = 64 +) + +// memDBIterator is a memDB iterator. +type memDBIterator struct { + ch <-chan *item + cancel context.CancelFunc + item *item + start []byte + end []byte + useMtx bool +} + +var _ Iterator = (*memDBIterator)(nil) + +// newMemDBIterator creates a new memDBIterator. +func newMemDBIterator(db *MemDB, start []byte, end []byte, reverse bool) *memDBIterator { + return newMemDBIteratorMtxChoice(db, start, end, reverse, true) +} + +func newMemDBIteratorMtxChoice(db *MemDB, start []byte, end []byte, reverse bool, useMtx bool) *memDBIterator { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan *item, chBufferSize) + iter := &memDBIterator{ + ch: ch, + cancel: cancel, + start: start, + end: end, + useMtx: useMtx, + } + + if useMtx { + db.mtx.RLock() + } + go func() { + if useMtx { + defer db.mtx.RUnlock() + } + // Because we use [start, end) for reverse ranges, while btree uses (start, end], we need + // the following variables to handle some reverse iteration conditions ourselves. + var ( + skipEqual []byte + abortLessThan []byte + ) + visitor := func(i btree.Item) bool { + item := i.(item) + if skipEqual != nil && bytes.Equal(item.key, skipEqual) { + skipEqual = nil + return true + } + if abortLessThan != nil && bytes.Compare(item.key, abortLessThan) == -1 { + return false + } + select { + case <-ctx.Done(): + return false + case ch <- &item: + return true + } + } + switch { + case start == nil && end == nil && !reverse: + db.btree.Ascend(visitor) + case start == nil && end == nil && reverse: + db.btree.Descend(visitor) + case end == nil && !reverse: + // must handle this specially, since nil is considered less than anything else + db.btree.AscendGreaterOrEqual(newKey(start), visitor) + case !reverse: + db.btree.AscendRange(newKey(start), newKey(end), visitor) + case end == nil: + // abort after start, since we use [start, end) while btree uses (start, end] + abortLessThan = start + db.btree.Descend(visitor) + default: + // skip end and abort after start, since we use [start, end) while btree uses (start, end] + skipEqual = end + abortLessThan = start + db.btree.DescendLessOrEqual(newKey(end), visitor) + } + close(ch) + }() + + // prime the iterator with the first value, if any + if item, ok := <-ch; ok { + iter.item = item + } + + return iter +} + +// Close implements Iterator. +func (i *memDBIterator) Close() error { + i.cancel() + for range i.ch { //nolint:revive + } // drain channel + i.item = nil + return nil +} + +// Domain implements Iterator. +func (i *memDBIterator) Domain() ([]byte, []byte) { + return i.start, i.end +} + +// Valid implements Iterator. +func (i *memDBIterator) Valid() bool { + return i.item != nil +} + +// Next implements Iterator. +func (i *memDBIterator) Next() { + i.assertIsValid() + item, ok := <-i.ch + switch { + case ok: + i.item = item + default: + i.item = nil + } +} + +// Error implements Iterator. +func (i *memDBIterator) Error() error { + return nil // famous last words +} + +// Key implements Iterator. +func (i *memDBIterator) Key() []byte { + i.assertIsValid() + return i.item.key +} + +// Value implements Iterator. +func (i *memDBIterator) Value() []byte { + i.assertIsValid() + return i.item.value +} + +func (i *memDBIterator) assertIsValid() { + if !i.Valid() { + panic("iterator is invalid") + } +} + +// memDBBatch operations +type opType int + +const ( + opTypeSet opType = iota + 1 + opTypeDelete +) + +type operation struct { + opType + key []byte + value []byte +} + +// memDBBatch handles in-memory batching. +type memDBBatch struct { + db *MemDB + ops []operation + size int +} + +var _ Batch = (*memDBBatch)(nil) + +// newMemDBBatch creates a new memDBBatch +func newMemDBBatch(db *MemDB) *memDBBatch { + return &memDBBatch{ + db: db, + ops: []operation{}, + size: 0, + } +} + +// Set implements Batch. +func (b *memDBBatch) Set(key, value []byte) error { + if len(key) == 0 { + return errKeyEmpty + } + if value == nil { + return errValueNil + } + if b.ops == nil { + return errBatchClosed + } + b.size += len(key) + len(value) + b.ops = append(b.ops, operation{opTypeSet, key, value}) + return nil +} + +// Delete implements Batch. +func (b *memDBBatch) Delete(key []byte) error { + if len(key) == 0 { + return errKeyEmpty + } + if b.ops == nil { + return errBatchClosed + } + b.size += len(key) + b.ops = append(b.ops, operation{opTypeDelete, key, nil}) + return nil +} + +// Write implements Batch. +func (b *memDBBatch) Write() error { + if b.ops == nil { + return errBatchClosed + } + b.db.mtx.Lock() + defer b.db.mtx.Unlock() + + for _, op := range b.ops { + switch op.opType { + case opTypeSet: + b.db.set(op.key, op.value) + case opTypeDelete: + b.db.delete(op.key) + default: + return fmt.Errorf("unknown operation type %v (%v)", op.opType, op) + } + } + + // Make sure batch cannot be used afterwards. Callers should still call Close(), for errors. + return b.Close() +} + +// WriteSync implements Batch. +func (b *memDBBatch) WriteSync() error { + return b.Write() +} + +// Close implements Batch. +func (b *memDBBatch) Close() error { + b.ops = nil + b.size = 0 + return nil +} + +// GetByteSize implements Batch +func (b *memDBBatch) GetByteSize() (int, error) { + if b.ops == nil { + return 0, errBatchClosed + } + return b.size, nil +} diff --git a/iavl/db/types.go b/iavl/db/types.go new file mode 100644 index 000000000..840494f41 --- /dev/null +++ b/iavl/db/types.go @@ -0,0 +1,137 @@ +package db + +import "errors" + +var ( + // errBatchClosed is returned when a closed or written batch is used. + errBatchClosed = errors.New("batch has been written or closed") + + // errKeyEmpty is returned when attempting to use an empty or nil key. + errKeyEmpty = errors.New("key cannot be empty") + + // errValueNil is returned when attempting to set a nil value. + errValueNil = errors.New("value cannot be nil") +) + +// DB is the main interface for all database backends. DBs are concurrency-safe. Callers must call +// Close on the database when done. +// +// Keys cannot be nil or empty, while values cannot be nil. Keys and values should be considered +// read-only, both when returned and when given, and must be copied before they are modified. +type DB interface { + // Get fetches the value of the given key, or nil if it does not exist. + // CONTRACT: key, value readonly []byte + Get([]byte) ([]byte, error) + + // Has checks if a key exists. + // CONTRACT: key, value readonly []byte + Has(key []byte) (bool, error) + + // Iterator returns an iterator over a domain of keys, in ascending order. The caller must call + // Close when done. End is exclusive, and start must be less than end. A nil start iterates + // from the first key, and a nil end iterates to the last key (inclusive). Empty keys are not + // valid. + // CONTRACT: No writes may happen within a domain while an iterator exists over it. + // CONTRACT: start, end readonly []byte + Iterator(start, end []byte) (Iterator, error) + + // ReverseIterator returns an iterator over a domain of keys, in descending order. The caller + // must call Close when done. End is exclusive, and start must be less than end. A nil end + // iterates from the last key (inclusive), and a nil start iterates to the first key (inclusive). + // Empty keys are not valid. + // CONTRACT: No writes may happen within a domain while an iterator exists over it. + // CONTRACT: start, end readonly []byte + ReverseIterator(start, end []byte) (Iterator, error) + + // Close closes the database connection. + Close() error + + // NewBatch creates a batch for atomic updates. The caller must call Batch.Close. + NewBatch() Batch + + // NewBatchWithSize create a new batch for atomic updates, but with pre-allocated size. + // This will does the same thing as NewBatch if the batch implementation doesn't support pre-allocation. + NewBatchWithSize(int) Batch +} + +// Iterator represents an iterator over a domain of keys. Callers must call Close when done. +// No writes can happen to a domain while there exists an iterator over it, some backends may take +// out database locks to ensure this will not happen. +// +// Callers must make sure the iterator is valid before calling any methods on it, otherwise +// these methods will panic. This is in part caused by most backend databases using this convention. +// +// As with DB, keys and values should be considered read-only, and must be copied before they are +// modified. +// +// Typical usage: +// +// var itr Iterator = ... +// defer itr.Close() +// +// for ; itr.Valid(); itr.Next() { +// k, v := itr.Key(); itr.Value() +// ... +// } +// +// if err := itr.Error(); err != nil { +// ... +// } +type Iterator interface { + // Domain returns the start (inclusive) and end (exclusive) limits of the iterator. + // CONTRACT: start, end readonly []byte + Domain() (start []byte, end []byte) + + // Valid returns whether the current iterator is valid. Once invalid, the Iterator remains + // invalid forever. + Valid() bool + + // Next moves the iterator to the next key in the database, as defined by order of iteration. + // If Valid returns false, this method will panic. + Next() + + // Key returns the key at the current position. Panics if the iterator is invalid. + // CONTRACT: key readonly []byte + Key() (key []byte) + + // Value returns the value at the current position. Panics if the iterator is invalid. + // CONTRACT: value readonly []byte + Value() (value []byte) + + // Error returns the last error encountered by the iterator, if any. + Error() error + + // Close closes the iterator, relasing any allocated resources. + Close() error +} + +// Batch represents a group of writes. They may or may not be written atomically depending on the +// backend. Callers must call Close on the batch when done. +// +// As with DB, given keys and values should be considered read-only, and must not be modified after +// passing them to the batch. +type Batch interface { + // Set sets a key/value pair. + // CONTRACT: key, value readonly []byte + Set(key, value []byte) error + + // Delete deletes a key/value pair. + // CONTRACT: key readonly []byte + Delete(key []byte) error + + // Write writes the batch, possibly without flushing to disk. Only Close() can be called after, + // other methods will error. + Write() error + + // WriteSync writes the batch and flushes it to disk. Only Close() can be called after, other + // methods will error. + WriteSync() error + + // Close closes the batch. It is idempotent, but calls to other methods afterwards will error. + Close() error + + // GetByteSize that returns the current size of the batch in bytes. Depending on the implementation, + // this may return the size of the underlying LSM batch, including the size of additional metadata + // on top of the expected key and value total byte count. + GetByteSize() (int, error) +} diff --git a/iavl/db/wrapper.go b/iavl/db/wrapper.go new file mode 100644 index 000000000..fce80f692 --- /dev/null +++ b/iavl/db/wrapper.go @@ -0,0 +1,44 @@ +package db + +import dbm "github.com/cosmos/cosmos-db" + +// Wrapper wraps a dbm.DB to implement DB. +type Wrapper struct { + dbm.DB +} + +var _ DB = (*Wrapper)(nil) + +// NewWrapper returns a new Wrapper. +func NewWrapper(db dbm.DB) *Wrapper { + return &Wrapper{DB: db} +} + +// Iterator implements DB. +func (db *Wrapper) Iterator(start, end []byte) (Iterator, error) { + return db.DB.Iterator(start, end) +} + +// ReverseIterator implements DB. +func (db *Wrapper) ReverseIterator(start, end []byte) (Iterator, error) { + return db.DB.ReverseIterator(start, end) +} + +// NewBatch implements DB. +func (db *Wrapper) NewBatch() Batch { + return db.DB.NewBatch() +} + +// NewBatchWithSize implements DB. +func (db *Wrapper) NewBatchWithSize(size int) Batch { + return db.DB.NewBatchWithSize(size) +} + +// NewDB returns a new Wrapper. +func NewDB(name, backendType, dir string) (*Wrapper, error) { + db, err := dbm.NewDB(name, dbm.BackendType(backendType), dir) + if err != nil { + return nil, err + } + return NewWrapper(db), nil +} diff --git a/iavl/diff.go b/iavl/diff.go new file mode 100644 index 000000000..8820af25b --- /dev/null +++ b/iavl/diff.go @@ -0,0 +1,152 @@ +package iavl + +import ( + "bytes" + + "github.com/cosmos/iavl/proto" +) + +type ( + KVPair = proto.KVPair + ChangeSet = proto.ChangeSet +) + +// KVPairReceiver is callback parameter of method `extractStateChanges` to receive stream of `KVPair`s. +type KVPairReceiver func(pair *KVPair) error + +// extractStateChanges extracts the state changes by between two versions of the tree. +// it first traverse the `root` tree until the first `sharedNode` and record the new leave nodes, +// then traverse the `prevRoot` tree until the current `sharedNode` to find out orphaned leave nodes, +// compare orphaned leave nodes and new leave nodes to produce stream of `KVPair`s and passed to callback. +// +// The algorithm don't run in constant memory strictly, but it tried the best the only +// keep minimal intermediate states in memory. +func (ndb *nodeDB) extractStateChanges(prevVersion int64, prevRoot, root []byte, receiver KVPairReceiver) error { + curIter, err := NewNodeIterator(root, ndb) + if err != nil { + return err + } + + prevIter, err := NewNodeIterator(prevRoot, ndb) + if err != nil { + return err + } + + var ( + // current shared node between two versions + sharedNode *Node + // record the newly added leaf nodes during the traversal to the `sharedNode`, + // will be compared with found orphaned nodes to produce change set stream. + newLeaves []*Node + ) + + // consumeNewLeaves concumes remaining `newLeaves` nodes and produce insertion `KVPair`. + consumeNewLeaves := func() error { + for _, node := range newLeaves { + if err := receiver(&KVPair{ + Key: node.key, + Value: node.value, + }); err != nil { + return err + } + } + + newLeaves = newLeaves[:0] + return nil + } + + // advanceSharedNode forward `curIter` until the next `sharedNode`, + // `sharedNode` will be `nil` if the new version is exhausted. + // it also records the new leaf nodes during the traversal. + advanceSharedNode := func() error { + if err := consumeNewLeaves(); err != nil { + return err + } + + sharedNode = nil + for curIter.Valid() { + node := curIter.GetNode() + shared := node.nodeKey.version <= prevVersion + curIter.Next(shared) + if shared { + sharedNode = node + break + } else if node.isLeaf() { + newLeaves = append(newLeaves, node) + } + } + + return nil + } + if err := advanceSharedNode(); err != nil { + return err + } + + // addOrphanedLeave receives a new orphaned leave node found in previous version, + // compare with the current newLeaves, to produce `iavl.KVPair` stream. + addOrphanedLeave := func(orphaned *Node) error { + for len(newLeaves) > 0 { + newLeave := newLeaves[0] + switch bytes.Compare(orphaned.key, newLeave.key) { + case 1: + // consume a new node as insertion and continue + newLeaves = newLeaves[1:] + if err := receiver(&KVPair{ + Key: newLeave.key, + Value: newLeave.value, + }); err != nil { + return err + } + continue + + case -1: + // removal, don't consume new nodes + return receiver(&KVPair{ + Delete: true, + Key: orphaned.key, + }) + + case 0: + // update, consume the new node and stop + newLeaves = newLeaves[1:] + return receiver(&KVPair{ + Key: newLeave.key, + Value: newLeave.value, + }) + } + } + + // removal + return receiver(&KVPair{ + Delete: true, + Key: orphaned.key, + }) + } + + // Traverse `prevIter` to find orphaned nodes in the previous version, + // and compare them with newLeaves to generate `KVPair` stream. + for prevIter.Valid() { + node := prevIter.GetNode() + shared := sharedNode != nil && (node == sharedNode || bytes.Equal(node.hash, sharedNode.hash)) + // skip sub-tree of shared nodes + prevIter.Next(shared) + if shared { + if err := advanceSharedNode(); err != nil { + return err + } + } else if node.isLeaf() { + if err := addOrphanedLeave(node); err != nil { + return err + } + } + } + + if err := consumeNewLeaves(); err != nil { + return err + } + + if err := curIter.Error(); err != nil { + return err + } + return prevIter.Error() +} diff --git a/iavl/diff_test.go b/iavl/diff_test.go new file mode 100644 index 000000000..0cdf080f2 --- /dev/null +++ b/iavl/diff_test.go @@ -0,0 +1,103 @@ +package iavl + +import ( + "encoding/binary" + "fmt" + "math" + "math/rand" + "sort" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +// TestDiffRoundTrip generate random change sets, build an iavl tree versions, +// then extract state changes from the versions and compare with the original change sets. +func TestDiffRoundTrip(t *testing.T) { + changeSets := genChangeSets(rand.New(rand.NewSource(0)), 300) + + // apply changeSets to tree + db := dbm.NewMemDB() + tree := NewMutableTree(db, 0, true, log.NewNopLogger()) + for i := range changeSets { + v, err := tree.SaveChangeSet(changeSets[i]) + require.NoError(t, err) + require.Equal(t, int64(i+1), v) + } + + // extract change sets from db + var extractChangeSets []*ChangeSet + tree2 := NewImmutableTree(db, 0, true, log.NewNopLogger()) + err := tree2.TraverseStateChanges(0, math.MaxInt64, func(version int64, changeSet *ChangeSet) error { + extractChangeSets = append(extractChangeSets, changeSet) + return nil + }) + require.NoError(t, err) + require.Equal(t, changeSets, extractChangeSets) +} + +func genChangeSets(r *rand.Rand, n int) []*ChangeSet { + var changeSets []*ChangeSet + + for i := 0; i < n; i++ { + items := make(map[string]*KVPair) + start, count, step := r.Int63n(1000), r.Int63n(1000), r.Int63n(10) + for i := start; i < start+count*step; i += step { + value := make([]byte, 8) + binary.LittleEndian.PutUint64(value, uint64(i)) + + key := fmt.Sprintf("test-%d", i) + items[key] = &KVPair{ + Key: []byte(key), + Value: value, + } + } + if len(changeSets) > 0 { + // pick some random keys to delete from the last version + lastChangeSet := changeSets[len(changeSets)-1] + count = r.Int63n(10) + for _, pair := range lastChangeSet.Pairs { + if count <= 0 { + break + } + if pair.Delete { + continue + } + items[string(pair.Key)] = &KVPair{ + Key: pair.Key, + Delete: true, + } + count-- + } + + // Special case, set to identical value + if len(lastChangeSet.Pairs) > 0 { + i := r.Int63n(int64(len(lastChangeSet.Pairs))) + pair := lastChangeSet.Pairs[i] + if !pair.Delete { + items[string(pair.Key)] = &KVPair{ + Key: pair.Key, + Value: pair.Value, + } + } + } + } + + var keys []string + for key := range items { + keys = append(keys, key) + } + sort.Strings(keys) + + var cs ChangeSet + for _, key := range keys { + cs.Pairs = append(cs.Pairs, items[key]) + } + + changeSets = append(changeSets, &cs) + } + return changeSets +} diff --git a/iavl/doc.go b/iavl/doc.go new file mode 100644 index 000000000..75203eed8 --- /dev/null +++ b/iavl/doc.go @@ -0,0 +1,52 @@ +// Package iavl implements a versioned, snapshottable (immutable) AVL+ tree +// for persisting key-value pairs. +// +// The tree is not safe for concurrent use, and must be guarded by a Mutex +// or RWLock as appropriate - the exception is immutable trees returned by +// MutableTree.GetImmutable() which are safe for concurrent use as long as +// the version is not deleted via DeleteVersion(). +// +// Basic usage of MutableTree: +// +// import "github.com/cosmos/iavl" +// import "github.com/cosmos/cosmos-db" +// ... +// +// tree := iavl.NewMutableTree(db.NewMemDB(), 128) +// +// tree.IsEmpty() // true +// +// tree.Set([]byte("alice"), []byte("abc")) +// tree.SaveVersion(1) +// +// tree.Set([]byte("alice"), []byte("xyz")) +// tree.Set([]byte("bob"), []byte("xyz")) +// tree.SaveVersion(2) +// +// tree.LatestVersion() // 2 +// +// tree.GetVersioned([]byte("alice"), 1) // "abc" +// tree.GetVersioned([]byte("alice"), 2) // "xyz" +// +// Proof of existence: +// +// root := tree.Hash() +// val, proof, err := tree.GetVersionedWithProof([]byte("bob"), 2) // "xyz", RangeProof, nil +// proof.Verify([]byte("bob"), val, root) // nil +// +// Proof of absence: +// +// _, proof, err = tree.GetVersionedWithProof([]byte("tom"), 2) // nil, RangeProof, nil +// proof.Verify([]byte("tom"), nil, root) // nil +// +// Now we delete an old version: +// +// tree.DeleteVersion(1) +// tree.VersionExists(1) // false +// tree.Get([]byte("alice")) // "xyz" +// tree.GetVersioned([]byte("alice"), 1) // nil +// +// Can't create a proof of absence for a version we no longer have: +// +// _, proof, err = tree.GetVersionedWithProof([]byte("tom"), 1) // nil, nil, error +package iavl diff --git a/iavl/docs/architecture/README.md b/iavl/docs/architecture/README.md new file mode 100644 index 000000000..a4def9afc --- /dev/null +++ b/iavl/docs/architecture/README.md @@ -0,0 +1,24 @@ +# Architecture Decision Records (ADR) + +This is a location to record all high-level architecture decisions for the Golang IAVL library. + +You can read more about the ADR concept in this [blog post](https://product.reverb.com/documenting-architecture-decisions-the-reverb-way-a3563bb24bd0#.78xhdix6t). + +An ADR should provide: + +- Context on the relevant goals and the current state +- Proposed changes to achieve the goals +- Summary of pros and cons +- References +- Changelog + +Note the distinction between an ADR and a spec. The ADR provides the context, intuition, reasoning, and +justification for a change in architecture, or for the architecture of something new. The spec is +much more compressed and streamlined summary of everything as it stands today. + +If recorded decisions turned out to be lacking, convene a discussion, record the new decisions here, +and then modify the code to match. + +## ADR Table of Contents + +- [ADR 001: Node Key Refactoring](./adr-001-node-key-refactoring.md) diff --git a/iavl/docs/architecture/adr-001-node-key-refactoring.md b/iavl/docs/architecture/adr-001-node-key-refactoring.md new file mode 100644 index 000000000..62bb4cdd5 --- /dev/null +++ b/iavl/docs/architecture/adr-001-node-key-refactoring.md @@ -0,0 +1,142 @@ +# ADR ADR-001: Node Key Refactoring + +## Changelog + +- 2022-10-31: First draft + +## Status + +Proposed + +## Context + +The original key format of IAVL nodes is a hash of the node. It does not take advantage of data locality on LSM-Tree. Nodes are stored with the random hash value, so it increases the number of compactions and makes it difficult to find the node. The new key format will take advantage of data locality in the LSM tree and reduce the number of compactions. + +The `orphans` are used to manage node removal in the current design and allow the deletion of removed nodes for the specific version from the disk through the `DeleteVersion` API. It needs to track every time when updating the tree and also requires extra storage to store `orphans`. But there are only 2 use cases for `DeleteVersion`: + +1. Rollback of the tree to a previous version +2. Remove unnecessary old nodes + +## Decision + +- Use the version and the local nonce as a node key like `bigendian(version) | bigendian(nonce)` format. Here the `nonce` is a local sequence id for the same version. + - Store the children node keys (`leftNodeKey` and `rightNodeKey`) in the node body. + - Remove the `version` field from node body writes. + - Remove the `leftHash` and `rightHash` fields, and instead store `hash` field in the node body. +- Remove the `orphans` completely from both tree and storage. + +New node structure + +```go +type NodeKey struct { + version int64 + nonce int32 +} + +type Node struct { + key []byte + value []byte + hash []byte // keep it in the storage instead of leftHash and rightHash + nodeKey *NodeKey // new field, the key in the storage + leftNodeKey *NodeKey // new field, need to store in the storage + rightNodeKey *NodeKey // new field, need to store in the storage + leftNode *Node + rightNode *Node + size int64 + leftNode *Node + rightNode *Node + subtreeHeight int8 +} +``` + +New tree structure + +```go +type MutableTree struct { + *ImmutableTree // The current, working tree. + lastSaved *ImmutableTree // The most recently saved tree. + unsavedFastNodeAdditions map[string]*fastnode.Node // FastNodes that have not yet been saved to disk + unsavedFastNodeRemovals map[string]interface{} // FastNodes that have not yet been removed from disk + ndb *nodeDB + skipFastStorageUpgrade bool // If true, the tree will work like no fast storage and always not upgrade fast storage + + mtx sync.Mutex +} +``` + +We will assign the `nodeKey` when saving the current version in `SaveVersion`. It will reduce unnecessary checks in CRUD operations of the tree and keep sorted the order of insertion in the LSM tree. + +### Migration + +We can migrate nodes through the following steps: + +- Export the snapshot of the tree from the original version. +- Import the snapshot to the new version. + - Track the nonce for the same version using int32 array of the version length. + - Assign the `nodeKey` when saving the node. + +### Pruning + +The current pruning strategies allows for intermediate versions to exist. With the adoption of this ADR we are migrating to allowing only versions to exist between a range (50-100 instead of 1,25,50-100). + +Here we are introducing a new way how to get orphaned nodes which remove in the `n+1`th version updates without storing orphanes in the storage. + +When we want to remove the `n+1`th version + +- Traverse the tree in-order way based on the root of `n+1`th version. +- If we visit the lower version node, pick the node and don't visit further deeply. Pay attention to the order of these nodes. +- Traverse the tree in-order way based on the root of `n`th version. +- Iterate the tree until meet the first node among the above nodes(stack) and delete all visited nodes so far from `n`th tree. +- Pop the first node from the stack and iterate again. + +If we assume `1 to (n-1)` versions already been removed, when we want to remove the `n`th version, we can just remove the above orphaned nodes. + +### Rollback + +When we want to rollback to the specific version `n` + +- Iterate the version from `n+1`. +- Traverse key-value through `traversePrefix` with `prefix=bigendian(version)`. +- Remove all iterated nodes. + +## Consequences + +### Positive + +* Using the version and a local nonce, we take advantage of data locality in the LSM tree. Since we commit the sorted data, it can reduce compactions and makes it easy to find the key. Also, it can reduce the key and node size in the storage. + + ``` + # node body + + add `hash`: +32 byte + add `leftNodeKey`, `rightNodeKey`: max (8 + 4) * 2 = +24 byte + remove `leftHash`, `rightHash`: -64 byte + remove `version`: max -8 byte + ------------------------------------------------------------ + total save 16 byte + + # node key + + remove `hash`: -32 byte + add `version|nonce`: +12 byte + ------------------------------------ + total save 20 byte + ``` + +* Removing orphans also provides performance improvements including memory and storage saving. + +### Negative + +* `Update` operations will require extra DB access because we need to take children to calculate the hash of updated nodes. + * It doesn't require more access in other cases including `Set`, `Remove`, and `Proof`. + +* It is impossible to remove the individual version. The new design requires more restrict pruning strategies. + +* When importing the tree, it may require more memory because of int32 array of the version length. We will introduce the new importing strategy to reduce the memory usage. + +## References + +- https://github.com/cosmos/iavl/issues/548 +- https://github.com/cosmos/iavl/issues/137 +- https://github.com/cosmos/iavl/issues/571 +- https://github.com/cosmos/cosmos-sdk/issues/12989 diff --git a/iavl/docs/architecture/adr-template.md b/iavl/docs/architecture/adr-template.md new file mode 100644 index 000000000..dae4dfd44 --- /dev/null +++ b/iavl/docs/architecture/adr-template.md @@ -0,0 +1,60 @@ +# ADR {ADR-NUMBER}: {TITLE} + +## Changelog + +* {date}: {changelog} + +## Status + +{DRAFT | PROPOSED} Not Implemented + +> Please have a look at the [PROCESS](./PROCESS.md#adr-status) page. +> Use DRAFT if the ADR is in a draft stage (draft PR) or PROPOSED if it's in review. + +## Abstract + +> "If you can't explain it simply, you don't understand it well enough." Provide a simplified and layman-accessible explanation of the ADR. +> A short (~200 word) description of the issue being addressed. + +## Context + +> This section describes the forces at play, including technological, political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts. It should clearly explain the problem and motivation that the proposal aims to resolve. +> {context body} + +## Decision + +> This section describes our response to these forces. It is stated in full sentences, with active voice. "We will ..." +> {decision body} + +## Consequences + +> This section describes the resulting context, after applying the decision. All consequences should be listed here, not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future. + +### Backwards Compatibility + +> All ADRs that introduce backwards incompatibilities must include a section describing these incompatibilities and their severity. The ADR must explain how the author proposes to deal with these incompatibilities. ADR submissions without a sufficient backwards compatibility treatise may be rejected outright. + +### Positive + +{positive consequences} + +### Negative + +{negative consequences} + +### Neutral + +{neutral consequences} + +## Further Discussions + +While an ADR is in the DRAFT or PROPOSED stage, this section should contain a summary of issues to be solved in future iterations (usually referencing comments from a pull-request discussion). +Later, this section can optionally list ideas or improvements the author or reviewers found during the analysis of this ADR. + +## Test Cases [optional] + +Test cases for an implementation are mandatory for ADRs that are affecting consensus changes. Other ADRs can choose to include links to test cases if applicable. + +## References + +* {reference link} diff --git a/iavl/docs/node/key_format.md b/iavl/docs/node/key_format.md new file mode 100644 index 000000000..4318c1651 --- /dev/null +++ b/iavl/docs/node/key_format.md @@ -0,0 +1,15 @@ +# Key Format + +Nodes and fastNodes are stored under the database with different key formats to ensure there are no key collisions and a structured key from which we can extract useful information. + +## Nodes + +Node KeyFormat: `n|node.nodeKey.version|node.nodeKey.nonce` + +Nodes are marshalled and stored under nodekey with prefix `n` to prevent collisions and then appended with the node's hash. + +### FastNodes + +FastNode KeyFormat: `f|node.key` + +FastNodes are marshalled nodes stored with prefix `f` to prevent collisions. You can extract fast nodes from the database by iterating over the keys with prefix `f`. diff --git a/iavl/docs/node/node.md b/iavl/docs/node/node.md new file mode 100644 index 000000000..78f9e40a8 --- /dev/null +++ b/iavl/docs/node/node.md @@ -0,0 +1,151 @@ +# Node + +The Node struct stores a node in the IAVL tree. + +## Structure + +```golang +// NodeKey represents a key of node in the DB. +type NodeKey struct { + version int64 // version of the IAVL that this node was first added in + nonce int32 // local nonce for the same version +} + +// Node represents a node in a Tree. +type Node struct { + key []byte // key for the node. + value []byte // value of leaf node. If inner node, value = nil + hash []byte // hash of above field and left node's hash, right node's hash + nodeKey *NodeKey // node key of the nodeDB + leftNodeKey *NodeKey // node key of the left child + rightNodeKey *NodeKey // node key of the right child + size int64 // number of leaves that are under the current node. Leaf nodes have size = 1 + leftNode *Node // pointer to left child + rightNode *Node // pointer to right child + subtreeHeight int8 // height of the node. Leaf nodes have height 0 +} +``` + +Inner nodes have keys equal to the highest key on the subtree and have values set to nil. + +The version of a node is the first version of the IAVL tree that the node gets added in. Future versions of the IAVL may point to this node if they also contain the node, however the node's version itself does not change. + +Size is the number of leaves under a given node. With a full subtree, `node.size = 2^(node.height)`. + +### Marshaling + +Every node is persisted by encoding the key, height, and size. If the node is a leaf node, then the value is persisted as well. If the node is not a leaf node, then the hash, leftNodeKey, and rightNodeKey are persisted as well. The hash should be persisted in inner nodes to avoid recalculating the hash when the node is loaded from the disk, if not persisted, we should iterate through the entire subtree to calculate the hash. + +```golang +// Writes the node as a serialized byte slice to the supplied io.Writer. +func (node *Node) writeBytes(w io.Writer) error { + if node == nil { + return errors.New("cannot write nil node") + } + err := encoding.EncodeVarint(w, int64(node.subtreeHeight)) + if err != nil { + return fmt.Errorf("writing height, %w", err) + } + err = encoding.EncodeVarint(w, node.size) + if err != nil { + return fmt.Errorf("writing size, %w", err) + } + + // Unlike writeHashByte, key is written for inner nodes. + err = encoding.EncodeBytes(w, node.key) + if err != nil { + return fmt.Errorf("writing key, %w", err) + } + + if node.isLeaf() { + err = encoding.EncodeBytes(w, node.value) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + err = encoding.EncodeBytes(w, node.hash) + if err != nil { + return fmt.Errorf("writing hash, %w", err) + } + if node.leftNodeKey == nil { + return ErrLeftNodeKeyEmpty + } + err = encoding.EncodeVarint(w, node.leftNodeKey.version) + if err != nil { + return fmt.Errorf("writing the version of left node key, %w", err) + } + err = encoding.EncodeVarint(w, int64(node.leftNodeKey.nonce)) + if err != nil { + return fmt.Errorf("writing the nonce of left node key, %w", err) + } + + if node.rightNodeKey == nil { + return ErrRightNodeKeyEmpty + } + err = encoding.EncodeVarint(w, node.rightNodeKey.version) + if err != nil { + return fmt.Errorf("writing the version of right node key, %w", err) + } + err = encoding.EncodeVarint(w, int64(node.rightNodeKey.nonce)) + if err != nil { + return fmt.Errorf("writing the nonce of right node key, %w", err) + } + } + return nil +} +``` + +### Hashes + +A node's hash is calculated by hashing the height, size, and version of the node. If the node is a leaf node, then the key and value are also hashed. If the node is an inner node, the leftHash and rightHash are included in hash but the key is not. + +```golang +// Writes the node's hash to the given io.Writer. This function expects +// child hashes to be already set. +func (node *Node) writeHashBytes(w io.Writer, version int64) error { + err := encoding.EncodeVarint(w, int64(node.subtreeHeight)) + if err != nil { + return fmt.Errorf("writing height, %w", err) + } + err = encoding.EncodeVarint(w, node.size) + if err != nil { + return fmt.Errorf("writing size, %w", err) + } + err = encoding.EncodeVarint(w, version) + if err != nil { + return fmt.Errorf("writing version, %w", err) + } + + // Key is not written for inner nodes, unlike writeBytes. + + if node.isLeaf() { + err = encoding.EncodeBytes(w, node.key) + if err != nil { + return fmt.Errorf("writing key, %w", err) + } + + // Indirection needed to provide proofs without values. + // (e.g. ProofLeafNode.ValueHash) + valueHash := sha256.Sum256(node.value) + + err = encoding.EncodeBytes(w, valueHash[:]) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + if node.leftNode == nil || node.rightNode == nil { + return ErrEmptyChild + } + err = encoding.EncodeBytes(w, node.leftNode.hash) + if err != nil { + return fmt.Errorf("writing left hash, %w", err) + } + err = encoding.EncodeBytes(w, node.rightNode.hash) + if err != nil { + return fmt.Errorf("writing right hash, %w", err) + } + } + + return nil +} +``` diff --git a/iavl/docs/node/nodedb.md b/iavl/docs/node/nodedb.md new file mode 100644 index 000000000..8f23bf439 --- /dev/null +++ b/iavl/docs/node/nodedb.md @@ -0,0 +1,165 @@ +# NodeDB + +## Structure + +The nodeDB is responsible for persisting nodes and fastNodes correctly in persistent storage. + +### Saving Versions + +It marshals and saves any new node that has been created under: `n|node.nodeKey.version|node.nodeKey.nonce`. For more details on how the node gets marshaled, see [node documentation](./node.md). The root of each version is saved under `n|version|1`. + +(For more details on key formats see the [keyformat docs](./key_format.md)) + +### Deleting Versions + +When a version `v` is deleted, all nodes which removed in the current version will be safely deleted and uncached from the storage. `nodeDB` will keep the range of versions [`fromVersion`, `toVersion`]. There are two apis to delete versions: + +#### DeleteVersionsFrom + +```golang +// DeleteVersionsFrom permanently deletes all tree versions from the given version upwards. +func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { + latest, err := ndb.getLatestVersion() + if err != nil { + return err + } + if latest < fromVersion { + return nil + } + + ndb.mtx.Lock() + for v, r := range ndb.versionReaders { + if v >= fromVersion && r != 0 { + return fmt.Errorf("unable to delete version %v with %v active readers", v, r) + } + } + ndb.mtx.Unlock() + + // Delete the nodes + err = ndb.traverseRange(nodeKeyFormat.Key(fromVersion), nodeKeyFormat.Key(latest+1), func(k, v []byte) error { + if err = ndb.batch.Delete(k); err != nil { + return err + } + return nil + }) + + if err != nil { + return err + } + + // NOTICE: we don't touch fast node indexes here, because it'll be rebuilt later because of version mismatch. + + ndb.resetLatestVersion(fromVersion - 1) + + return nil +} +``` + +#### DeleteVersionsTo + +```golang +// DeleteVersionsTo deletes the oldest versions up to the given version from disk. +func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { + first, err := ndb.getFirstVersion() + if err != nil { + return err + } + + latest, err := ndb.getLatestVersion() + if err != nil { + return err + } + + if toVersion < first || latest <= toVersion { + return fmt.Errorf("the version should be in the range of [%d, %d)", first, latest) + } + + for v, r := range ndb.versionReaders { + if v >= first && v <= toVersion && r != 0 { + return fmt.Errorf("unable to delete version %v with %v active readers", v, r) + } + } + + for version := first; version <= toVersion; version++ { + if err := ndb.deleteVersion(version); err != nil { + return err + } + ndb.resetFirstVersion(version + 1) + } + + return nil +} + +// deleteVersion deletes a tree version from disk. +// deletes orphans +func (ndb *nodeDB) deleteVersion(version int64) error { + rootKey, err := ndb.GetRoot(version) + if err != nil { + return err + } + if rootKey == nil || rootKey.version < version { + if err := ndb.batch.Delete(ndb.nodeKey(&NodeKey{version: version, nonce: 1})); err != nil { + return err + } + } + + return ndb.traverseOrphans(version, func(orphan *Node) error { + return ndb.batch.Delete(ndb.nodeKey(orphan.nodeKey)) + }) +} +``` + +##### Travesing Orphans + +The traverseOrphans algorithm is shown below: + +```golang +// traverseOrphans traverses orphans which removed by the updates of the version (n+1). +func (ndb *nodeDB) traverseOrphans(version int64, fn func(*Node) error) error { + curKey, err := ndb.GetRoot(version + 1) + if err != nil { + return err + } + + curIter, err := NewNodeIterator(curKey, ndb) + if err != nil { + return err + } + + prevKey, err := ndb.GetRoot(version) + if err != nil { + return err + } + prevIter, err := NewNodeIterator(prevKey, ndb) + if err != nil { + return err + } + + var orgNode *Node + for prevIter.Valid() { + for orgNode == nil && curIter.Valid() { + node := curIter.GetNode() + if node.nodeKey.version <= version { + curIter.Next(true) + orgNode = node + } else { + curIter.Next(false) + } + } + pNode := prevIter.GetNode() + + if orgNode != nil && bytes.Equal(pNode.hash, orgNode.hash) { + prevIter.Next(true) + orgNode = nil + } else { + err = fn(pNode) + if err != nil { + return err + } + prevIter.Next(false) + } + } + + return nil +} +``` diff --git a/iavl/docs/overview.md b/iavl/docs/overview.md new file mode 100644 index 000000000..02f32fc40 --- /dev/null +++ b/iavl/docs/overview.md @@ -0,0 +1,42 @@ +# IAVL Spec + +The IAVL tree is a versioned, snapshottable (immutable) AVL+ tree for persistent data. + +The purpose of this data structure is to provide persistent storage for key-value pairs (say to store account balances) such that a deterministic merkle root hash can be computed. The tree is balanced using a variant of the [AVL algorithm](http://en.wikipedia.org/wiki/AVL_tree) so all operations are O(log(n)). + +Nodes of this tree are immutable and indexed by their hash. Thus any node serves as an immutable snapshot which lets us stage uncommitted transactions from the mempool cheaply, and we can instantly roll back to the last committed state to process transactions of a newly committed block (which may not be the same set of transactions as those from the mempool). + +In an AVL tree, the heights of the two child subtrees of any node differ by at most one. Whenever this condition is violated upon an update, the tree is rebalanced by creating O(log(n)) new nodes that point to unmodified nodes of the old tree. In the original AVL algorithm, inner nodes can also hold key-value pairs. The AVL+ algorithm (note the plus) modifies the AVL algorithm to keep all values on leaf nodes, while only using branch-nodes to store keys. This simplifies the algorithm while keeping the merkle hash trail short. + +The IAVL tree will typically be wrapped by a `MutableTree` to enable updates to the tree. Any changes between versions get persisted to disk while nodes that exist in both the old version and new version are simply pointed to by the respective tree without duplicated the node data. + +When a node is no longer part of the latest IAVL tree, it is called an orphan. The orphaned node will exist in the nodeDB so long as there are versioned IAVL trees that are persisted in nodeDB that contain the orphaned node. Once all trees that referred to orphaned node have been deleted from database, the orphaned node will also get deleted. + +In Ethereum, the analog is [Patricia tries](http://en.wikipedia.org/wiki/Radix_tree). There are tradeoffs. Keys do not need to be hashed prior to insertion in IAVL+ trees, so this provides faster iteration in the key space which may benefit some applications. The logic is simpler to implement, requiring only two types of nodes -- inner nodes and leaf nodes. On the other hand, while IAVL+ trees provide a deterministic merkle root hash, it depends on the order of transactions. In practice this shouldn't be a problem, since you can efficiently encode the tree structure when serializing the tree contents. + +### Suggested Order for Understanding IAVL + +1. [Node docs](./node/node.md) + - Explains node structure + - Explains how node gets marshalled and hashed +2. [KeyFormat docs](./node/key_format.md) + - Explains keyformats for how nodes, orphans, and roots are stored under formatted keys in database +3. [NodeDB docs](./node/nodedb.md): + - Explains how nodes, orphans, roots get saved in database + - Explains saving and deleting tree logic. +4. [ImmutableTree docs](./tree/immutable_tree.md) + - Explains ImmutableTree structure + - Explains ImmutableTree Iteration functions +5. [MutableTree docs](./tree/mutable_tree.md) + - Explains MutableTree structure + - Explains how to make updates (set/delete) to current working tree of IAVL + - Explains how automatic rebalancing of IAVL works + - Explains how Saving and Deleting versions of IAVL works +6. [Proof docs](./proof/proof.md) + - Explains what Merkle proofs are + - Explains how IAVL supports presence, absence, and range proofs + - Explains the IAVL proof data structures +7. [Export/import docs](./tree/export_import.md) + - Explains the overall export/import functionality + - Explains the `ExportNode` format for exported nodes + - Explains the algorithms for exporting and importing nodes \ No newline at end of file diff --git a/iavl/docs/proof/proof.md b/iavl/docs/proof/proof.md new file mode 100644 index 000000000..991c22c1b --- /dev/null +++ b/iavl/docs/proof/proof.md @@ -0,0 +1,347 @@ +# Proofs + +What sets IAVL apart from most other key/value stores is the ability to return +[Merkle proofs](https://en.wikipedia.org/wiki/Merkle_tree) along with values. These proofs can +be used to verify that a returned value is, in fact, the value contained within a given IAVL tree. +This verification is done by comparing the proof's root hash with the tree's root hash. + +Somewhat simplified, an IAVL tree is a variant of a +[binary search tree](https://en.wikipedia.org/wiki/Binary_search_tree) where inner nodes contain +keys used for binary search, and leaf nodes contain the actual key/value pairs ordered by key. +Consider the following example, containing five key/value pairs (such as key `a` with value `1`): + +``` + d + / \ + c e + / \ / \ + b c=3 d=4 e=5 + / \ +a=1 b=2 +``` + +In reality, IAVL nodes contain more data than shown here - for details please refer to the +[node documentation](../node/node.md). However, this simplified version is sufficient for an +overview. + +A cryptographically secure hash is generated for each node in the tree by hashing the node's key +and value (if leaf node), version, and height, as well as the hashes of each direct child (if +any). This implies that the hash of any given node also depends on the hashes of all descendants +of the node. In turn, this implies that the hash of the root node depends on the hashes of all +nodes (and therefore all data) in the tree. + +If we fetch the value `a=1` from the tree and want to verify that this is the correct value, we +need the following information: + +``` + d + / \ + c hash=d6f56d + / \ + b hash=ec6088 + / \ +a,hash(1) hash=92fd030 +``` + +Note that we take the hash of the value of `a=1` instead of simply using the value `1` itself; +both would work, but the value can be arbitrarily large while the hash has a constant size. + +With this data, we are able to compute the hashes for all nodes up to and including the root, +and can compare this root hash with the root hash of the IAVL tree - if they match, we can be +reasonably certain that the provided value is the same as the value in the tree. This data is +therefore considered a _proof_ for the value. Notice how we don't need to include any data from +e.g. the `e`-branch of the tree at all, only the hash - as the tree grows in size, these savings +become very significant, requiring only `log₂(n)` hashes for a tree of `n` keys. + +However, this still introduces quite a bit of overhead. Since we usually want to fetch several +values from the tree and verify them, it is often useful to generate a _range proof_, which can +prove any and all key/value pairs within a contiguous, ordered key range. For example, the +following proof can verify both `a=1`, `b=2`, and `c=3`: + +``` + d + / \ + c hash=d6f56d + / \ + b c,hash(3) + / \ +a,hash(1) b,hash(2) +``` + +Range proofs can also prove the _absence_ of any keys within the range. For example, the above +proof can prove that the key `ab` is not in the tree, because if it was it would have to be +ordered between `a` and `b` - it is clear from the proof that there is no such node, and if +there was it would cause the parent hashes to be different from what we see. + +Range proofs can be generated for non-existant endpoints by including the nearest neighboring +keys, which allows them to cover any arbitrary key range. This can also be used to generate an +absence proof for a _single_ non-existant key, by returning a range proof between the two nearest +neighbors. The range proof is therefore a complete proof for all existing and all absent key/value +pairs ordered between two arbitrary endpoints. + +Note that the IAVL terminology for range proofs may differ from that used in other systems, where +it refers to proofs that a value lies within some interval without revealing the exact value. IAVL +range proofs are used to prove which key/value pairs exist (or not) in some key range, and may be +known as range queries elsewhere. + +## API Overview + +The following is a general overview of the API - for details, see the +[API reference](https://pkg.go.dev/github.com/cosmos/iavl). + +As an example, we will be using the same IAVL tree as described in the introduction: + +``` + d + / \ + c e + / \ / \ + b c=3 d=4 e=5 + / \ +a=1 b=2 +``` + +This tree can be generated as follows: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/cosmos/iavl" + db "github.com/cosmos/cosmos-db" +) + +func main() { + tree, err := iavl.NewMutableTree(db.NewMemDB(), 0) + if err != nil { + log.Fatal(err) + } + + tree.Set([]byte("e"), []byte{5}) + tree.Set([]byte("d"), []byte{4}) + tree.Set([]byte("c"), []byte{3}) + tree.Set([]byte("b"), []byte{2}) + tree.Set([]byte("a"), []byte{1}) + + rootHash, version, err := tree.SaveVersion() + if err != nil { + log.Fatal(err) + } + fmt.Printf("Saved version %v with root hash %x\n", version, rootHash) + + // Output tree structure, including all node hashes (prefixed with 'n') + fmt.Println(tree.String()) +} +``` + +### Tree Root Hash + +Proofs are verified against the root hash of an IAVL tree. This root hash is retrived via +`MutableTree.Hash()` or `ImmutableTree.Hash()`, returning a `[]byte` hash. It is also returned by +`MutableTree.SaveVersion()`, as shown above. + +```go +fmt.Printf("%x\n", tree.Hash()) +// Outputs: dd21329c026b0141e76096b5df395395ae3fc3293bd46706b97c034218fe2468 +``` + +### Generating Proofs + +The following methods are used to generate proofs, all of which are of type `RangeProof`: + +* `ImmutableTree.GetWithProof(key []byte)`: fetches the key's value (if it exists) along with a + proof of existence or proof of absence. + +* `ImmutableTree.GetRangeWithProof(start, end []byte, limit int)`: fetches the keys, values, and + proofs for the given key range, optionally with a limit (end key is excluded). + +* `MutableTree.GetVersionedWithProof(key []byte, version int64)`: like `GetWithProof()`, but for a + specific version of the tree. + +* `MutableTree.GetVersionedRangeWithProof(key []byte, version int64)`: like `GetRangeWithProof()`, + but for a specific version of the tree. + +### Verifying Proofs + +The following `RangeProof` methods are used to verify proofs: + +* `Verify(rootHash []byte)`: verify that the proof root hash matches the given tree root hash. + +* `VerifyItem(key, value []byte)`: verify that the given key exists with the given value, according + to the proof. + +* `VerifyAbsent(key []byte)`: verify that the given key is absent, according to the proof. + +To verify that a `RangeProof` is valid for a given IAVL tree (i.e. that the proof root hash matches +the tree root hash), run `RangeProof.Verify()` with the tree's root hash: + +```go +// Generate a proof for a=1 +value, proof, err := tree.GetWithProof([]byte("a")) +if err != nil { + log.Fatal(err) +} + +// Verify that the proof's root hash matches the tree's +err = proof.Verify(tree.Hash()) +if err != nil { + log.Fatalf("Invalid proof: %v", err) +} +``` + +The proof must always be verified against the root hash with `Verify()` before attempting other +operations. The proof can also be verified manually with `RangeProof.ComputeRootHash()`: + +```go +if !bytes.Equal(proof.ComputeRootHash(), tree.Hash()) { + log.Fatal("Proof hash mismatch") +} +``` + +To verify that a key has a given value according to the proof, use `VerifyItem()` on a proof +generated for this key (or key range): + +```go +// The proof was generated for the item a=1, so this is successful +err = proof.VerifyItem([]byte("a"), []byte{1}) +fmt.Printf("prove a=1: %v\n", err) +// outputs nil + +// If we instead claim that a=2, the proof will error +err = proof.VerifyItem([]byte("a"), []byte{2}) +fmt.Printf("prove a=2: %v\n", err) +// outputs "leaf value hash not same: invalid proof" + +// Also, verifying b=2 errors even though it is correct, since the proof is for a=1 +err = proof.VerifyItem([]byte("b"), []byte{2}) +fmt.Printf("prove b=2: %v\n", err) +// outputs "leaf key not found in proof: invalid proof" +``` + +If we generate a proof for a range of keys, we can use this both to prove the value of any of the +keys in the range as well as the absence of any keys that would have been within it: + +```go +// Note that the end key is not inclusive, so c is not in the proof. 0 means +// no key limit (all keys). +keys, values, proof, err := tree.GetRangeWithProof([]byte("a"), []byte("c"), 0) +if err != nil { + log.Fatal(err) +} + +err = proof.Verify(tree.Hash()) +if err != nil { + log.Fatal(err) +} + +// Prove that a=1 is in the range +err = proof.VerifyItem([]byte("a"), []byte{1}) +fmt.Printf("prove a=1: %v\n", err) +// outputs nil + +// Prove that b=2 is also in the range +err = proof.VerifyItem([]byte("b"), []byte{2}) +fmt.Printf("prove b=2: %v\n", err) +// outputs nil + +// Since "ab" is ordered after "a" but before "b", we can prove that it +// is not in the range and therefore not in the tree at all +err = proof.VerifyAbsence([]byte("ab")) +fmt.Printf("prove no ab: %v\n", err) +// outputs nil + +// If we try to prove ab, we get an error: +err = proof.VerifyItem([]byte("ab"), []byte{0}) +fmt.Printf("prove ab=0: %v\n", err) +// outputs "leaf key not found in proof: invalid proof" +``` + +### Proof Structure + +The overall proof structure was described in the introduction. Here, we will have a look at the +actual data structure. Knowledge of this is not necessary to use proofs. It may also be useful +to have a look at the [`Node` data structure](../node/node.md). + +Recall our example tree: + +``` + d + / \ + c e + / \ / \ + b c=3 d=4 e=5 + / \ +a=1 b=2 +``` + +A `RangeProof` contains the following data, as well as JSON tags for serialization: + +```go +type RangeProof struct { + LeftPath PathToLeaf `json:"left_path"` + InnerNodes []PathToLeaf `json:"inner_nodes"` + Leaves []ProofLeafNode `json:"leaves"` +} +``` + +* `LeftPath` contains the path to the leftmost node in the proof. For a proof of the range `a` to + `e` (excluding `e=5`), it contains information about the inner nodes `d`, `c`, and `b` in that + order. + +* `InnerNodes` contains paths with any additional inner nodes not already in `LeftPath`, with `nil` + paths for nodes already traversed. For a proof of the range `a` to `e` (excluding `e=5`), this + contains the paths `nil`, `nil`, `[e]` where the `nil` paths refer to the paths to `b=2` and + `c=3` already traversed in `LeftPath`, and `[e]` contains data about the `e` inner node needed + to prove `d=4`. + +* `Leaves` contains data about the leaf nodes in the range. For the range `a` to `e` (exluding + `e=5`) this contains info about `a=1`, `b=2`, `c=3`, and `d=4` in left-to-right order. + +Note that `Leaves` may contain additional leaf nodes outside the requested range, for example to +satisfy absence proofs if a given key does not exist. This may require additional inner nodes +to be included as well. + +`PathToLeaf` is simply a slice of `ProofInnerNode`: + +```go +type PathToLeaf []ProofInnerNode +``` + +Where `ProofInnerNode` contains the following data (a subset of the [node data](../node/node.md)): + +```go +type ProofInnerNode struct { + Height int8 `json:"height"` + Size int64 `json:"size"` + Version int64 `json:"version"` + Left []byte `json:"left"` + Right []byte `json:"right"` +} +``` + +Unlike in our diagrams, the key of the inner nodes are not actually part of the proof. This is +because they are only used to guide binary searches and do not necessarily correspond to actual keys +in the data set, and are thus not included in any hashes. + +Similarly, `ProofLeafNode` contains a subset of leaf node data: + +```go +type ProofLeafNode struct { + Key cmn.HexBytes `json:"key"` + ValueHash cmn.HexBytes `json:"value"` + Version int64 `json:"version"` +} +``` + +Notice how the proof contains a hash of the node's value rather than the value itself. This is +because values can be arbitrarily large while the hash has a constant size. The Merkle hashes of +the tree are computed in the same way, by hashing the value before including it in the node +hash. + +The information in these proofs is sufficient to reasonably prove that a given value exists (or +does not exist) in a given version of an IAVL dataset without fetching the entire dataset, requiring +only `log₂(n)` hashes for a dataset of `n` items. For more information, please see the +[API reference](https://pkg.go.dev/github.com/cosmos/iavl). \ No newline at end of file diff --git a/iavl/docs/tree/export_import.md b/iavl/docs/tree/export_import.md new file mode 100644 index 000000000..180b3f7d0 --- /dev/null +++ b/iavl/docs/tree/export_import.md @@ -0,0 +1,64 @@ +# Export/Import + +A single `ImmutableTree` (i.e. a single version) can be exported via `ImmutableTree.Export()`, returning an iterator over `ExportNode` items. These nodes can be imported into an empty `MutableTree` with `MutableTree.Import()` to recreate an identical tree. The structure of `ExportNode` is: + +```go +type ExportNode struct { + Key []byte + Value []byte + Version int64 + Height int8 +} +``` + +This is the minimum amount of data about nodes that can be exported, see the [node documentation](../node/node.md) for comparison. The other node attributes, such as `hash` and `size`, can be derived from this data. Both leaf nodes and inner nodes are exported, since `Version` is part of the hash and inner nodes have different versions than the leaf nodes with the same key. + +The order of exported nodes is significant. Nodes are exported by depth-first post-order (LRN) tree traversal. Consider the following tree (with nodes in `key@version=value` format): + +``` + d@3 + / \ + c@3 e@3 + / \ / \ + b@3 c@3=3 d@2=4 e@3=5 + / \ +a@1=1 b@3=2 + +``` + +This would produce the following export: + +```go +[]*ExportNode{ + {Key: []byte("a"), Value: []byte{1}, Version: 1, Height: 0}, + {Key: []byte("b"), Value: []byte{2}, Version: 3, Height: 0}, + {Key: []byte("b"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("c"), Value: []byte{3}, Version: 3, Height: 0}, + {Key: []byte("c"), Value: nil, Version: 3, Height: 2}, + {Key: []byte("d"), Value: []byte{4}, Version: 2, Height: 0}, + {Key: []byte("e"), Value: []byte{5}, Version: 3, Height: 0}, + {Key: []byte("e"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("d"), Value: nil, Version: 3, Height: 3}, +} +``` + +When importing, the tree must be rebuilt in the same order, such that the missing attributes (e.g. `hash` and `size`) can be generated. This is possible because children are always given before their parents. We can therefore first generate the hash and size of the left and right leaf nodes, and then use these to recursively generate the hash and size of the parent. + +One way to do this is to keep a stack of determined children, and then pop those children once we build their parent, which then becomes a new child on the stack. We know that we encounter a parent because its height is higher than the child or children on top of the stack. We need a stack because we may need to recursively build a right branch while holding an determined left child. For the above export this would look like the following (in `key:height=value` format): + +``` +| Stack | Import node | +|-----------------|-------------------------------------------------------------| +| | {Key: []byte("a"), Value: []byte{1}, Version: 1, Height: 0} | +| a:0=1 | {Key: []byte("b"), Value: []byte{2}, Version: 3, Height: 0} | +| a:0=1,b:0=2 | {Key: []byte("b"), Value: nil, Version: 3, Height: 1} | +| b:1 | {Key: []byte("c"), Value: []byte{3}, Version: 3, Height: 0} | +| b:1,c:0=3 | {Key: []byte("c"), Value: nil, Version: 3, Height: 2} | +| c:2 | {Key: []byte("d"), Value: []byte{4}, Version: 2, Height: 0} | +| c:2,d:0=4 | {Key: []byte("e"), Value: []byte{5}, Version: 3, Height: 0} | +| c:2,d:0=4,e:0=5 | {Key: []byte("e"), Value: nil, Version: 3, Height: 1} | +| c:2,e:1 | {Key: []byte("d"), Value: nil, Version: 3, Height: 3} | +| d:3 | | +``` + +At the end, there will be a single node left on the stack, which is the root node of the tree. \ No newline at end of file diff --git a/iavl/docs/tree/immutable_tree.md b/iavl/docs/tree/immutable_tree.md new file mode 100644 index 000000000..6e73e5b17 --- /dev/null +++ b/iavl/docs/tree/immutable_tree.md @@ -0,0 +1,70 @@ +# Immutable Tree + +### Structure + +The Immutable tree struct contains an IAVL version. + +```golang +type ImmutableTree struct { + root *Node + ndb *nodeDB + version int64 + skipFastStorageUpgrade bool +} +``` + +Using the root and the nodeDB, the ImmutableTree can retrieve any node that is a part of the IAVL tree at this version. + +Users can get information about the IAVL tree by calling getter functions such as `Size()` and `Height()` which will return the tree's size and height by querying the root node's size and height. + +### Get + +Users can get values by specifying the key or the index of the leaf node they want to get value for. + +GetWithIndex by key will return both the index and the value. + +```golang +// GetWithIndex returns the index and value of the specified key if it exists, or nil +// and the next index, if it doesn't. +func (t *ImmutableTree) GetWithIndex(key []byte) (int64, []byte, error) { + if t.root == nil { + return 0, nil, nil + } + return t.root.get(t, key) +} +``` + +Get by index will return both the key and the value. The index is the index in the list of leaf nodes sorted lexicographically by key. The leftmost leaf has index 0. It's neighbor has index 1 and so on. + +```golang +// GetByIndex gets the key and value at the specified index. +func (t *ImmutableTree) GetByIndex(index int64) (key []byte, value []byte) { + if t.root == nil { + return nil, nil + } + return t.root.getByIndex(t, index) +} +``` + +### Iterating + +Iteration works by traversing from the root node. All iteration functions are provided a callback function `func(key, value []byte) (stop bool`). This callback is called on every leaf node's key and value in order of the iteration. If the callback returns true, then the iteration stops. Otherwise it continues. + +Thus the callback is useful both as a way to run some logic on every key-value pair stored in the IAVL and as a way to dynamically stop the iteration. + +The `IterateRange` functions allow users to iterate over a specific range and specify if the iteration should be in ascending or descending order. + +The API's for Iteration functions are shown below. + +```golang +// Iterate iterates over all keys of the tree, in order. +func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool) + +// IterateRange makes a callback for all nodes with key between start and end non-inclusive. +// If either are nil, then it is open on that side (nil, nil is the same as Iterate) +func (t *ImmutableTree) IterateRange(start, end []byte, ascending bool, fn func(key []byte, value []byte) bool) (stopped bool) + +// IterateRangeInclusive makes a callback for all nodes with key between start and end inclusive. +// If either are nil, then it is open on that side (nil, nil is the same as Iterate) +func (t *ImmutableTree) IterateRangeInclusive(start, end []byte, ascending bool, fn func(key, value []byte, version int64) bool) (stopped bool) +``` diff --git a/iavl/docs/tree/mutable_tree.md b/iavl/docs/tree/mutable_tree.md new file mode 100644 index 000000000..9ce62eb1c --- /dev/null +++ b/iavl/docs/tree/mutable_tree.md @@ -0,0 +1,348 @@ +# Mutable Tree + +### Structure + +The MutableTree struct is a wrapper around ImmutableTree to allow for updates that get stored in successive versions. + +The MutableTree stores the last saved ImmutableTree and the current working tree in its struct while all other saved, available versions are accessible from the nodeDB. + +```golang +// MutableTree is a persistent tree which keeps track of versions. +type MutableTree struct { + *ImmutableTree // The current, working tree. + lastSaved *ImmutableTree // The most recently saved tree. + unsavedFastNodeAdditions map[string]*fastnode.Node // FastNodes that have not yet been saved to disk + unsavedFastNodeRemovals map[string]interface{} // FastNodes that have not yet been removed from disk + ndb *nodeDB + skipFastStorageUpgrade bool // If true, the tree will work like no fast storage and always not upgrade fast storage + + mtx sync.Mutex +} +``` + +### Set + +Set can be used to add a new key-value pair to the IAVL tree, or to update an existing key with a new value. + +Set starts at the root of the IAVL tree, if the key is less than or equal to the root key, it recursively calls set on root's left child. Else, it recursively calls set on root's right child. It continues to recurse down the IAVL tree based on comparing the set key and the node key until it reaches a leaf node. + +If the leaf node has the same key as the set key, then the set is just updating an existing key with a new value. The value is updated, and the old version of the node is orphaned. + +If the leaf node does not have the same key as the set key, then the set is trying to add a new key to the IAVL tree. The leaf node is replaced by an inner node that has the original leaf node and the new node from the set call as its children. + +If the `setKey` < `leafKey`: + +```golang +// new leaf node that gets created by Set +// since this is a new update since latest saved version, +// this node has version=latestVersion+1 +newVersion := latestVersion+1 +newNode := NewNode(key, value, newVersion) +// original leaf node: originalLeaf gets replaced by inner node below +Node{ + key: leafKey, // inner node key is equal to right child's key + height: 1, // height=1 since node is parent of leaves + size: 2, // 2 leaf nodes under this node + leftNode: newNode, // left Node is the new added leaf node + rightNode: originalLeaf, // right Node is the original leaf node +} +``` + +If `setKey` > `leafKey`: + +```golang +// new leaf node that gets created by Set +// since this is a new update since latest saved version, +// this node has version=latestVersion+1 +newVersion := latestVersion+1 +newNode := NewNode(key, value, newVersion) +// original leaf node: originalLeaf gets replaced by inner node below +Node{ + key: setKey, // inner node key is equal to right child's key + height: 1, // height=1 since node is parent of leaves + size: 2, // 2 leaf nodes under this node + leftNode: originalLeaf, // left Node is the original leaf node + rightNode: newNode, // right Node is the new added leaf node +} +``` + +Any node that gets recursed upon during a Set call is necessarily orphaned since it will either have a new value (in the case of an update) or it will have a new descendant. For new nodes, the node key and hash will be assigned in the `SaveVersion` (see `SaveVersion` section). + +After each set, the current working tree has its height and size recalculated. If the height of the left branch and right branch of the working tree differs by more than one, then the mutable tree has to be balanced before the Set call can return. + +### Remove + +Remove is another recursive function to remove a key-value pair from the IAVL pair. If the key that is trying to be removed does not exist, Remove is a no-op. + +Remove recurses down the IAVL tree in the same way that Set does until it reaches a leaf node. If the leaf node's key is equal to the remove key, the node is removed, and all of its parents are recursively updated. If not, the remove call does nothing. + +#### Recursive Remove + +Remove works by calling an inner function `recursiveRemove` that returns the following values after a recursive call `recursiveRemove(recurseNode, removeKey)`: + +##### ReplaceNode + +Just like with recursiveSet, any node that gets recursed upon (in a successful remove) will get orphaned since its hash must be updated and the nodes are immutable. Thus, ReplaceNode is the new node that replaces `recurseNode`. + +If recurseNode is the leaf that gets removed, then ReplaceNode is `nil`. + +If recurseNode is the direct parent of the leaf that got removed, then it can simply be replaced by the other child. Since the parent of recurseNode can directly refer to recurseNode's remaining child. For example if recurseNode's left child gets removed, the following happens: + + +Before LeftLeaf removed: +``` + |---RightLeaf +IAVLTREE---recurseNode--| + |---LeftLeaf +``` + +After LeftLeaf removed: +``` +IAVLTREE---RightLeaf + +ReplaceNode = RightLeaf +``` + +If recurseNode is an inner node that got called in the recursiveRemove, but is not a direct parent of the removed leaf. Then an updated version of the node will exist in the tree. Notably, it will have an incremented version, a new hash (as explained in the `NewHash` section), and recalculated height and size. + +The ReplaceNode will be a cloned version of `recurseNode` with an incremented version. The hash will be updated given the NewHash of recurseNode's left child or right child (depending on which branch got recurse upon). + +The height and size of the ReplaceNode will have to be calculated since these values can change after the `remove`. + +It's possible that the subtree for `ReplaceNode` will have to be rebalanced (see `Balance` section). If this is the case, this will also update `ReplaceNode`'s hash since the structure of `ReplaceNode`'s subtree will change. + +##### RemovedValue + +RemovedValue is the value that was at the node that was removed. It does not get changed as it travels up the recursive stack. + +If `removeKey` does not exist in the IAVL tree, RemovedValue is `nil`. + +### Balance + +Anytime a node is unbalanced such that the height of its left branch and the height of its right branch differs by more than 1, the IAVL tree will rebalance itself. + +This is acheived by rotating the subtrees until there is no more than one height difference between two branches of any subtree in the IAVL. + +Since Balance is mutating the structure of the tree, any displaced nodes will be orphaned. + +#### RotateRight + +To rotate right on a node `rotatedNode`, we first orphan its left child. We clone the left child to create a new node `newNode`. We set `newNode`'s right hash and child to the `rotatedNode`. We now set `rotatedNode`'s left child to be the old right child of `newNode`. + +Visualization (Nodes are numbered to show correct key order is still preserved): + +Before `RotateRight(node8)`: +``` + |---9 +8---| + | |---7 + | |---6 + | | |---5 + |---4 + | |---3 + |---2 + |---1 +``` + +After `RotateRight(node8)`: +``` + |---9 + |---8' + | | |---7 + | |---6 + | |---5 +4'---| + | |---3 + |---2 + |---1 + +Orphaned: 4, 8 +``` + +Note that the key order for subtrees is still preserved. + +#### RotateLeft + +Similarly, to rotate left on a node `rotatedNode` we first orphan its right child. We clone the right child to create a new node `newNode`. We set the `newNode`'s left hash and child to the `rotatedNode`. We then set the `rotatedNode`'s right child to be the old left child of the node. + +Before `RotateLeft(node2)`: +``` + |---9 + |---8 + | |---7 + |---6 + | | |---5 + | |---4 + | |---3 +2---| + |---1 +``` + +After `RotateLeft(node2)`: +``` + |---9 + |---8 + | |---7 +6'---| + | |---5 + | |---4 + | | |---3 + |---2' + |---1 + +Orphaned: 6, 2 +``` + +The IAVL detects whenever a subtree has become unbalanced by 2 (after any set/remove). If this does happen, then the tree is immediately rebalanced. Thus, any unbalanced subtree can only exist in 4 states: + +#### Left Left Case + +1. `RotateRight(node8)` + +**Before: Left Left Unbalanced** +``` + |---9 +8---| + | |---6 + |---4 + | |---3 + |---2 +``` + +**After 1: Balanced** +``` + |---9 + |---8' + | |---6 +4'---| + | |---3 + |---2 + +Orphaned: 4, 8 +``` + +#### Left Right Case + +Make tree left left unbalanced, and then balance. + +1. `RotateLeft(node4)` +2. `RotateRight(node8)` + +**Before: Left Right Unbalanced** +``` + |---9 +8---| + | |---6 + | | |---5 + |---4 + |---2 +``` + +**After 1: Left Left Unbalanced** +``` + |---9 +8'---| + |---6' + | |---5 + |---4 + |---2 + +Orphaned: 6, 8 +``` + +**After 2: Balanced** +``` + |---9 + |---8 +6'---| + | |---5 + |---4 + |---2 + +Orphaned: 6 +``` + +Note: 6 got orphaned again, so omit list repitition + +#### Right Right Case + +1. `RotateLeft(node2)` + +**Before: Right Right Unbalanced** +``` + |---9 + |---8 + |---6 + | |---4 +2---| + |---1 +``` + +**After: Balanced** +``` + |---9 + |---8 +6'---| + | |---4 + |---2' + |---1 + +Orphaned: 6, 2 +``` + +#### Right Left Case + +Make tree right right unbalanced, then balance. + +1. `RotateRight(6)` +2. `RotateLeft(2)` + +**Before: Right Left Unbalanced** +``` + |---8 + |---6 + | |---4 + | |---3 +2---| + |---1 +``` + +**After 1: Right Right Unbalanced** +``` + |---8 + |---6 + |---4' + | |---3 +2'---| + |---1 + +Orphaned: 4, 2 +``` + +**After 2: Balanced** +``` + |---8 + |---6 +4'---| + | |---3 + |---2 + |---1 + +Orphaned: 4 +``` + +### SaveVersion + +SaveVersion saves the current working tree as the latest version, `tree.version+1`. + +If the tree's root is empty, then there are no nodes to save. Then the `nodeDB` also saves the empty root for this version. + +If the root is not empty. Then `SaveVersion` will iterate the tree and save new nodes (which the node key is `nil`) to the `nodeDB`. + +`SaveVersion` also calls `nodeDB.Commit`, this ensures that any batched writes from the last save gets committed to the appropriate databases. + +`tree.version` gets incremented and it will set the lastSaved `ImmutableTree` to the current working tree, and clone the tree to allow for future updates on the next working tree. + +Lastly, it returns the tree's hash, the latest version, and nil for error. + +SaveVersion will error if a tree at the version trying to be saved already exists. diff --git a/iavl/export.go b/iavl/export.go new file mode 100644 index 000000000..af7f52649 --- /dev/null +++ b/iavl/export.go @@ -0,0 +1,99 @@ +package iavl + +import ( + "context" + "errors" + "fmt" +) + +// exportBufferSize is the number of nodes to buffer in the exporter. It improves throughput by +// processing multiple nodes per context switch, but take care to avoid excessive memory usage, +// especially since callers may export several IAVL stores in parallel (e.g. the Cosmos SDK). +const exportBufferSize = 32 + +// ErrorExportDone is returned by Exporter.Next() when all items have been exported. +var ErrorExportDone = errors.New("export is complete") + +// ErrNotInitalizedTree when chains introduce a store without initializing data +var ErrNotInitalizedTree = errors.New("iavl/export newExporter failed to create") + +// ExportNode contains exported node data. +type ExportNode struct { + Key []byte + Value []byte + Version int64 + Height int8 +} + +// Exporter exports nodes from an ImmutableTree. It is created by ImmutableTree.Export(). +// +// Exported nodes can be imported into an empty tree with MutableTree.Import(). Nodes are exported +// depth-first post-order (LRN), this order must be preserved when importing in order to recreate +// the same tree structure. +type Exporter struct { + tree *ImmutableTree + ch chan *ExportNode + cancel context.CancelFunc +} + +// NewExporter creates a new Exporter. Callers must call Close() when done. +func newExporter(tree *ImmutableTree) (*Exporter, error) { + if tree == nil { + return nil, fmt.Errorf("tree is nil: %w", ErrNotInitalizedTree) + } + // CV Prevent crash on incrVersionReaders if tree.ndb == nil + if tree.ndb == nil { + return nil, fmt.Errorf("tree.ndb is nil: %w", ErrNotInitalizedTree) + } + + ctx, cancel := context.WithCancel(context.Background()) + exporter := &Exporter{ + tree: tree, + ch: make(chan *ExportNode, exportBufferSize), + cancel: cancel, + } + + tree.ndb.incrVersionReaders(tree.version) + go exporter.export(ctx) + + return exporter, nil +} + +// export exports nodes +func (e *Exporter) export(ctx context.Context) { + e.tree.root.traversePost(e.tree, true, func(node *Node) bool { + exportNode := &ExportNode{ + Key: node.key, + Value: node.value, + Version: node.nodeKey.version, + Height: node.subtreeHeight, + } + + select { + case e.ch <- exportNode: + return false + case <-ctx.Done(): + return true + } + }) + close(e.ch) +} + +// Next fetches the next exported node, or returns ExportDone when done. +func (e *Exporter) Next() (*ExportNode, error) { + if exportNode, ok := <-e.ch; ok { + return exportNode, nil + } + return nil, ErrorExportDone +} + +// Close closes the exporter. It is safe to call multiple times. +func (e *Exporter) Close() { + e.cancel() + for range e.ch { //nolint:revive + } // drain channel + if e.tree != nil { + e.tree.ndb.decrVersionReaders(e.tree.version) + } + e.tree = nil +} diff --git a/iavl/export_test.go b/iavl/export_test.go new file mode 100644 index 000000000..3abd602e9 --- /dev/null +++ b/iavl/export_test.go @@ -0,0 +1,377 @@ +package iavl + +import ( + "math" + "math/rand" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +// setupExportTreeBasic sets up a basic tree with a handful of +// create/update/delete operations over a few versions. +func setupExportTreeBasic(t require.TestingT) *ImmutableTree { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + _, err := tree.Set([]byte("x"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("z"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, _, err = tree.Remove([]byte("x")) + require.NoError(t, err) + _, _, err = tree.Remove([]byte("b")) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{255}) + require.NoError(t, err) + _, err = tree.Set([]byte("d"), []byte{4}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, err = tree.Set([]byte("e"), []byte{5}) + require.NoError(t, err) + _, _, err = tree.Remove([]byte("z")) + require.NoError(t, err) + _, err = tree.Set([]byte("abc"), []byte{6}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + return itree +} + +// setupExportTreeRandom sets up a randomly generated tree. +// nolint: dupl +func setupExportTreeRandom(t *testing.T) *ImmutableTree { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + + versions = 8 // number of versions to generate + versionOps = 1024 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + r := rand.New(rand.NewSource(randSeed)) + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + var version int64 + keys := make([][]byte, 0, versionOps) + for i := 0; i < versions; i++ { + for j := 0; j < versionOps; j++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + + // The performance of this is likely to be terrible, but that's fine for small tests + switch { + case len(keys) > 0 && r.Float64() <= deleteRatio: + index := r.Intn(len(keys)) + key = keys[index] + keys = append(keys[:index], keys[index+1:]...) + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + + case len(keys) > 0 && r.Float64() <= updateRatio: + key = keys[r.Intn(len(keys))] + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + require.True(t, updated) + + default: + r.Read(key) + r.Read(value) + // If we get an update, set again + for updated, err := tree.Set(key, value); updated && err == nil; { + key = make([]byte, keySize) + r.Read(key) + } + keys = append(keys, key) + } + } + var err error + _, version, err = tree.SaveVersion() + require.NoError(t, err) + } + + require.EqualValues(t, versions, tree.Version()) + require.GreaterOrEqual(t, tree.Size(), int64(math.Trunc(versions*versionOps*(1-updateRatio-deleteRatio))/2)) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + return itree +} + +// setupExportTreeSized sets up a single-version tree with a given number +// of randomly generated key/value pairs, useful for benchmarking. +func setupExportTreeSized(t require.TestingT, treeSize int) *ImmutableTree { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + ) + + r := rand.New(rand.NewSource(randSeed)) + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + for i := 0; i < treeSize; i++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + r.Read(key) + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + + if updated { + i-- + } + } + + _, version, err := tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(version) + require.NoError(t, err) + + return itree +} + +func TestExporter(t *testing.T) { + tree := setupExportTreeBasic(t) + + expect := []*ExportNode{ + {Key: []byte("a"), Value: []byte{1}, Version: 1, Height: 0}, + {Key: []byte("abc"), Value: []byte{6}, Version: 3, Height: 0}, + {Key: []byte("abc"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("b"), Value: []byte{2}, Version: 3, Height: 0}, + {Key: []byte("c"), Value: []byte{3}, Version: 3, Height: 0}, + {Key: []byte("c"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("b"), Value: nil, Version: 3, Height: 2}, + {Key: []byte("d"), Value: []byte{4}, Version: 2, Height: 0}, + {Key: []byte("e"), Value: []byte{5}, Version: 3, Height: 0}, + {Key: []byte("e"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("d"), Value: nil, Version: 3, Height: 3}, + } + + actual := make([]*ExportNode, 0, len(expect)) + exporter, err := tree.Export() + require.NoError(t, err) + defer exporter.Close() + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + actual = append(actual, node) + } + + assert.Equal(t, expect, actual) +} + +func TestExporterCompress(t *testing.T) { + tree := setupExportTreeBasic(t) + + expect := []*ExportNode{ + {Key: []byte{0, 'a'}, Value: []byte{1}, Version: 1, Height: 0}, + {Key: []byte{1, 'b', 'c'}, Value: []byte{6}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: []byte{0, 'b'}, Value: []byte{2}, Version: 3, Height: 0}, + {Key: []byte{0, 'c'}, Value: []byte{3}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: nil, Value: nil, Version: 0, Height: 2}, + {Key: []byte{0, 'd'}, Value: []byte{4}, Version: 2, Height: 0}, + {Key: []byte{0, 'e'}, Value: []byte{5}, Version: 3, Height: 0}, + {Key: nil, Value: nil, Version: 0, Height: 1}, + {Key: nil, Value: nil, Version: 0, Height: 3}, + } + + actual := make([]*ExportNode, 0, len(expect)) + innerExporter, err := tree.Export() + require.NoError(t, err) + defer innerExporter.Close() + + exporter := NewCompressExporter(innerExporter) + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + actual = append(actual, node) + } + + assert.Equal(t, expect, actual) +} + +func TestExporter_Import(t *testing.T) { + testcases := map[string]*ImmutableTree{ + "empty tree": NewImmutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()), + "basic tree": setupExportTreeBasic(t), + } + if !testing.Short() { + testcases["sized tree"] = setupExportTreeSized(t, 4096) + testcases["random tree"] = setupExportTreeRandom(t) + } + + for desc, tree := range testcases { + tree := tree + for _, compress := range []bool{false, true} { + if compress { + desc += "-compress" + } + compress := compress + t.Run(desc, func(t *testing.T) { + t.Parallel() + + innerExporter, err := tree.Export() + require.NoError(t, err) + defer innerExporter.Close() + + exporter := NodeExporter(innerExporter) + if compress { + exporter = NewCompressExporter(innerExporter) + } + + newTree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + innerImporter, err := newTree.Import(tree.Version()) + require.NoError(t, err) + defer innerImporter.Close() + + importer := NodeImporter(innerImporter) + if compress { + importer = NewCompressImporter(innerImporter) + } + + for { + item, err := exporter.Next() + if err == ErrorExportDone { + err = innerImporter.Commit() + require.NoError(t, err) + break + } + require.NoError(t, err) + err = importer.Add(item) + require.NoError(t, err) + } + + treeHash := tree.Hash() + newTreeHash := newTree.Hash() + + require.Equal(t, treeHash, newTreeHash, "Tree hash mismatch") + require.Equal(t, tree.Size(), newTree.Size(), "Tree size mismatch") + require.Equal(t, tree.Version(), newTree.Version(), "Tree version mismatch") + + tree.Iterate(func(key, value []byte) bool { //nolint:errcheck + index, _, err := tree.GetWithIndex(key) + require.NoError(t, err) + newIndex, newValue, err := newTree.GetWithIndex(key) + require.NoError(t, err) + require.Equal(t, index, newIndex, "Index mismatch for key %v", key) + require.Equal(t, value, newValue, "Value mismatch for key %v", key) + return false + }) + }) + } + } +} + +func TestExporter_Close(t *testing.T) { + tree := setupExportTreeSized(t, 4096) + exporter, err := tree.Export() + require.NoError(t, err) + + node, err := exporter.Next() + require.NoError(t, err) + require.NotNil(t, node) + + exporter.Close() + node, err = exporter.Next() + require.Error(t, err) + require.Equal(t, ErrorExportDone, err) + require.Nil(t, node) + + node, err = exporter.Next() + require.Error(t, err) + require.Equal(t, ErrorExportDone, err) + require.Nil(t, node) + + exporter.Close() + exporter.Close() +} + +func TestExporter_DeleteVersionErrors(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("b"), []byte{2}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Set([]byte("c"), []byte{3}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + itree, err := tree.GetImmutable(2) + require.NoError(t, err) + exporter, err := itree.Export() + require.NoError(t, err) + defer exporter.Close() + + err = tree.DeleteVersionsTo(1) + require.NoError(t, err) + + err = tree.DeleteVersionsTo(2) + require.Error(t, err) + + exporter.Close() + err = tree.DeleteVersionsTo(2) + require.NoError(t, err) +} + +func BenchmarkExport(b *testing.B) { + b.StopTimer() + tree := setupExportTreeSized(b, 4096) + b.StartTimer() + for n := 0; n < b.N; n++ { + exporter, err := tree.Export() + require.NoError(b, err) + for { + _, err := exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + b.Error(err) + } + } + exporter.Close() + } +} diff --git a/iavl/fast_iterator.go b/iavl/fast_iterator.go new file mode 100644 index 000000000..1391cda7f --- /dev/null +++ b/iavl/fast_iterator.go @@ -0,0 +1,135 @@ +package iavl + +import ( + "errors" + + dbm "github.com/cosmos/cosmos-db" + + "github.com/cosmos/iavl/fastnode" +) + +var errFastIteratorNilNdbGiven = errors.New("fast iterator must be created with a nodedb but it was nil") + +// FastIterator is a dbm.Iterator for ImmutableTree +// it iterates over the latest state via fast nodes, +// taking advantage of keys being located in sequence in the underlying database. +type FastIterator struct { + start, end []byte + + valid bool + + ascending bool + + err error + + ndb *nodeDB + + nextFastNode *fastnode.Node + + fastIterator dbm.Iterator +} + +var _ dbm.Iterator = (*FastIterator)(nil) + +func NewFastIterator(start, end []byte, ascending bool, ndb *nodeDB) *FastIterator { + iter := &FastIterator{ + start: start, + end: end, + err: nil, + ascending: ascending, + ndb: ndb, + nextFastNode: nil, + fastIterator: nil, + } + // Move iterator before the first element + iter.Next() + return iter +} + +// Domain implements dbm.Iterator. +// Maps the underlying nodedb iterator domain, to the 'logical' keys involved. +func (iter *FastIterator) Domain() ([]byte, []byte) { + if iter.fastIterator == nil { + return iter.start, iter.end + } + + start, end := iter.fastIterator.Domain() + + if start != nil { + start = start[1:] + if len(start) == 0 { + start = nil + } + } + + if end != nil { + end = end[1:] + if len(end) == 0 { + end = nil + } + } + + return start, end +} + +// Valid implements dbm.Iterator. +func (iter *FastIterator) Valid() bool { + return iter.fastIterator != nil && iter.fastIterator.Valid() && iter.valid +} + +// Key implements dbm.Iterator +func (iter *FastIterator) Key() []byte { + if iter.valid { + return iter.nextFastNode.GetKey() + } + return nil +} + +// Value implements dbm.Iterator +func (iter *FastIterator) Value() []byte { + if iter.valid { + return iter.nextFastNode.GetValue() + } + return nil +} + +// Next implements dbm.Iterator +func (iter *FastIterator) Next() { + if iter.ndb == nil { + iter.err = errFastIteratorNilNdbGiven + iter.valid = false + return + } + + if iter.fastIterator == nil { + iter.fastIterator, iter.err = iter.ndb.getFastIterator(iter.start, iter.end, iter.ascending) + iter.valid = true + } else { + iter.fastIterator.Next() + } + + if iter.err == nil { + iter.err = iter.fastIterator.Error() + } + + iter.valid = iter.valid && iter.fastIterator.Valid() + if iter.valid { + iter.nextFastNode, iter.err = fastnode.DeserializeNode(iter.fastIterator.Key()[1:], iter.fastIterator.Value()) + iter.valid = iter.err == nil + } +} + +// Close implements dbm.Iterator +func (iter *FastIterator) Close() error { + if iter.fastIterator != nil { + iter.err = iter.fastIterator.Close() + } + iter.valid = false + iter.fastIterator = nil + return iter.err +} + +// Error implements dbm.Iterator +func (iter *FastIterator) Error() error { + return iter.err +} diff --git a/iavl/fastnode/fast_node.go b/iavl/fastnode/fast_node.go new file mode 100644 index 000000000..5d00bd997 --- /dev/null +++ b/iavl/fastnode/fast_node.go @@ -0,0 +1,86 @@ +package fastnode + +import ( + "errors" + "fmt" + "io" + + "github.com/cosmos/iavl/cache" + "github.com/cosmos/iavl/internal/encoding" +) + +// NOTE: This file favors int64 as opposed to int for size/counts. +// The Tree on the other hand favors int. This is intentional. + +type Node struct { + key []byte + versionLastUpdatedAt int64 + value []byte +} + +var _ cache.Node = (*Node)(nil) + +// NewNode returns a new fast node from a value and version. +func NewNode(key []byte, value []byte, version int64) *Node { + return &Node{ + key: key, + versionLastUpdatedAt: version, + value: value, + } +} + +// DeserializeNode constructs an *FastNode from an encoded byte slice. +// It assumes we do not mutate this input []byte. +func DeserializeNode(key []byte, buf []byte) (*Node, error) { + ver, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding fastnode.version, %w", err) + } + buf = buf[n:] + + val, _, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding fastnode.value, %w", err) + } + + fastNode := &Node{ + key: key, + versionLastUpdatedAt: ver, + value: val, + } + + return fastNode, nil +} + +func (fn *Node) GetKey() []byte { + return fn.key +} + +func (fn *Node) EncodedSize() int { + n := encoding.EncodeVarintSize(fn.versionLastUpdatedAt) + encoding.EncodeBytesSize(fn.value) + return n +} + +func (fn *Node) GetValue() []byte { + return fn.value +} + +func (fn *Node) GetVersionLastUpdatedAt() int64 { + return fn.versionLastUpdatedAt +} + +// WriteBytes writes the FastNode as a serialized byte slice to the supplied io.Writer. +func (fn *Node) WriteBytes(w io.Writer) error { + if fn == nil { + return errors.New("cannot write nil node") + } + err := encoding.EncodeVarint(w, fn.versionLastUpdatedAt) + if err != nil { + return fmt.Errorf("writing version last updated at, %w", err) + } + err = encoding.EncodeBytes(w, fn.value) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + return nil +} diff --git a/iavl/fastnode/fast_node_test.go b/iavl/fastnode/fast_node_test.go new file mode 100644 index 000000000..7c82758d3 --- /dev/null +++ b/iavl/fastnode/fast_node_test.go @@ -0,0 +1,60 @@ +package fastnode + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +func TestFastNode_encodedSize(t *testing.T) { + fastNode := &Node{ + key: iavlrand.RandBytes(10), + versionLastUpdatedAt: 1, + value: iavlrand.RandBytes(20), + } + + expectedSize := 1 + len(fastNode.value) + 1 + + require.Equal(t, expectedSize, fastNode.EncodedSize()) +} + +func TestFastNode_encode_decode(t *testing.T) { + testcases := map[string]struct { + node *Node + expectHex string + expectError bool + }{ + "nil": {nil, "", true}, + "empty": {&Node{}, "0000", false}, + "inner": {&Node{ + key: []byte{0x4}, + versionLastUpdatedAt: 1, + value: []byte{0x2}, + }, "020102", false}, + } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + var buf bytes.Buffer + err := tc.node.WriteBytes(&buf) + if tc.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectHex, hex.EncodeToString(buf.Bytes())) + + node, err := DeserializeNode(tc.node.key, buf.Bytes()) + require.NoError(t, err) + // since value and leafHash are always decoded to []byte{} we augment the expected struct here + if tc.node.value == nil { + tc.node.value = []byte{} + } + require.Equal(t, tc.node, node) + }) + } +} diff --git a/iavl/go.mod b/iavl/go.mod new file mode 100644 index 000000000..72d8e15df --- /dev/null +++ b/iavl/go.mod @@ -0,0 +1,56 @@ +module github.com/cosmos/iavl + +go 1.20 + +require ( + cosmossdk.io/log v1.2.0 + github.com/cosmos/cosmos-db v1.0.0 + github.com/cosmos/ics23/go v0.10.0 + github.com/emicklei/dot v1.4.2 + github.com/golang/mock v1.6.0 + github.com/google/btree v1.1.2 + github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.12.0 + google.golang.org/protobuf v1.30.0 +) + +require ( + github.com/DataDog/zstd v1.4.5 // indirect + github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cockroachdb/errors v1.8.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect + github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 // indirect + github.com/cockroachdb/redact v1.0.8 // indirect + github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect + github.com/cosmos/gogoproto v1.4.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/linxGnu/grocksdb v1.7.15 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/onsi/gomega v1.26.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/zerolog v1.30.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.11.0 // indirect + gonum.org/v1/gonum v0.11.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +retract ( + // This version is not used by the Cosmos SDK and adds a maintenance burden. + // Use v1.x.x instead. + [v0.21.0, v0.21.2] + v0.18.0 +) diff --git a/iavl/go.sum b/iavl/go.sum new file mode 100644 index 000000000..d4b1757a0 --- /dev/null +++ b/iavl/go.sum @@ -0,0 +1,418 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/log v1.2.0 h1:BbykkDsutXPSy8RojFB3KZEWyvMsToLy0ykb/ZhsLqQ= +cosmossdk.io/log v1.2.0/go.mod h1:GNSCc/6+DhFIj1aLn/j7Id7PaO8DzNylUZoOYBL9+I4= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= +github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= +github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= +github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= +github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 h1:qbb/AE938DFhOajUYh9+OXELpSF9KZw2ZivtmW6eX1Q= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0= +github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw= +github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/cosmos-db v1.0.0 h1:EVcQZ+qYag7W6uorBKFPvX6gRjw6Uq2hIh4hCWjuQ0E= +github.com/cosmos/cosmos-db v1.0.0/go.mod h1:iBvi1TtqaedwLdcrZVYRSSCb6eSy61NLj4UNmdIgs0U= +github.com/cosmos/gogoproto v1.4.3 h1:RP3yyVREh9snv/lsOvmsAPQt8f44LgL281X0IOIhhcI= +github.com/cosmos/gogoproto v1.4.3/go.mod h1:0hLIG5TR7IvV1fme1HCFKjfzW9X2x0Mo+RooWXCnOWU= +github.com/cosmos/ics23/go v0.10.0 h1:iXqLLgp2Lp+EdpIuwXTYIQU+AiHj9mOC2X9ab++bZDM= +github.com/cosmos/ics23/go v0.10.0/go.mod h1:ZfJSmng/TBNTBkFemHHHj5YY7VAU/MBU980F4VU1NG0= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/emicklei/dot v1.4.2 h1:UbK6gX4yvrpHKlxuUQicwoAis4zl8Dzwit9SnbBAXWw= +github.com/emicklei/dot v1.4.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +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 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +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/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= +github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= +github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/linxGnu/grocksdb v1.7.15 h1:AEhP28lkeAybv5UYNYviYISpR6bJejEnKuYbnWAnxx0= +github.com/linxGnu/grocksdb v1.7.15/go.mod h1:pY55D0o+r8yUYLq70QmhdudxYvoDb9F+9puf4m3/W+U= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= +github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +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.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +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/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.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/iavl/immutable_tree.go b/iavl/immutable_tree.go new file mode 100644 index 000000000..4825b493b --- /dev/null +++ b/iavl/immutable_tree.go @@ -0,0 +1,335 @@ +package iavl + +import ( + "fmt" + "strings" + + "cosmossdk.io/log" + + dbm "github.com/cosmos/iavl/db" +) + +// ImmutableTree contains the immutable tree at a given version. It is typically created by calling +// MutableTree.GetImmutable(), in which case the returned tree is safe for concurrent access as +// long as the version is not deleted via DeleteVersion() or the tree's pruning settings. +// +// Returned key/value byte slices must not be modified, since they may point to data located inside +// IAVL which would also be modified. +type ImmutableTree struct { + logger log.Logger + + root *Node + ndb *nodeDB + version int64 + skipFastStorageUpgrade bool +} + +// NewImmutableTree creates both in-memory and persistent instances +func NewImmutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, lg log.Logger, options ...Option) *ImmutableTree { + opts := DefaultOptions() + for _, opt := range options { + opt(&opts) + } + + if db == nil { + // In-memory Tree. + return &ImmutableTree{} + } + + return &ImmutableTree{ + logger: lg, + // NodeDB-backed Tree. + ndb: newNodeDB(db, cacheSize, opts, lg), + skipFastStorageUpgrade: skipFastStorageUpgrade, + } +} + +// String returns a string representation of Tree. +func (t *ImmutableTree) String() string { + leaves := []string{} + t.Iterate(func(key []byte, val []byte) (stop bool) { //nolint:errcheck + leaves = append(leaves, fmt.Sprintf("%x: %x", key, val)) + return false + }) + return "Tree{" + strings.Join(leaves, ", ") + "}" +} + +// RenderShape provides a nested tree shape, ident is prepended in each level +// Returns an array of strings, one per line, to join with "\n" or display otherwise +func (t *ImmutableTree) RenderShape(indent string, encoder NodeEncoder) ([]string, error) { + if encoder == nil { + encoder = defaultNodeEncoder + } + return t.renderNode(t.root, indent, 0, encoder) +} + +// NodeEncoder will take an id (hash, or key for leaf nodes), the depth of the node, +// and whether or not this is a leaf node. +// It returns the string we wish to print, for iaviwer +type NodeEncoder func(id []byte, depth int, isLeaf bool) string + +// defaultNodeEncoder can encode any node unless the client overrides it +func defaultNodeEncoder(id []byte, _ int, isLeaf bool) string { + prefix := "- " + if isLeaf { + prefix = "* " + } + if len(id) == 0 { + return fmt.Sprintf("%s", prefix) + } + return fmt.Sprintf("%s%X", prefix, id) +} + +func (t *ImmutableTree) renderNode(node *Node, indent string, depth int, encoder func([]byte, int, bool) string) ([]string, error) { + prefix := strings.Repeat(indent, depth) + // handle nil + if node == nil { + return []string{fmt.Sprintf("%s", prefix)}, nil + } + // handle leaf + if node.isLeaf() { + here := fmt.Sprintf("%s%s", prefix, encoder(node.key, depth, true)) + return []string{here}, nil + } + + // recurse on inner node + here := fmt.Sprintf("%s%s", prefix, encoder(node.hash, depth, false)) + + rightNode, err := node.getRightNode(t) + if err != nil { + return nil, err + } + + leftNode, err := node.getLeftNode(t) + if err != nil { + return nil, err + } + + right, err := t.renderNode(rightNode, indent, depth+1, encoder) + if err != nil { + return nil, err + } + + result, err := t.renderNode(leftNode, indent, depth+1, encoder) // left + if err != nil { + return nil, err + } + + result = append(result, here) + result = append(result, right...) + return result, nil +} + +// Size returns the number of leaf nodes in the tree. +func (t *ImmutableTree) Size() int64 { + if t.root == nil { + return 0 + } + return t.root.size +} + +// Version returns the version of the tree. +func (t *ImmutableTree) Version() int64 { + return t.version +} + +// Height returns the height of the tree. +func (t *ImmutableTree) Height() int8 { + if t.root == nil { + return 0 + } + return t.root.subtreeHeight +} + +// Has returns whether or not a key exists. +func (t *ImmutableTree) Has(key []byte) (bool, error) { + if t.root == nil { + return false, nil + } + return t.root.has(t, key) +} + +// Hash returns the root hash. +func (t *ImmutableTree) Hash() []byte { + fmt.Printf("TREE VERSION: %d\n", t.version+1) + return t.root.hashWithCount(t.version + 1) +} + +// Export returns an iterator that exports tree nodes as ExportNodes. These nodes can be +// imported with MutableTree.Import() to recreate an identical tree. +func (t *ImmutableTree) Export() (*Exporter, error) { + return newExporter(t) +} + +// GetWithIndex returns the index and value of the specified key if it exists, or nil and the next index +// otherwise. The returned value must not be modified, since it may point to data stored within +// IAVL. +// +// The index is the index in the list of leaf nodes sorted lexicographically by key. The leftmost leaf has index 0. +// It's neighbor has index 1 and so on. +func (t *ImmutableTree) GetWithIndex(key []byte) (int64, []byte, error) { + if t.root == nil { + return 0, nil, nil + } + return t.root.get(t, key) +} + +// Get returns the value of the specified key if it exists, or nil. +// The returned value must not be modified, since it may point to data stored within IAVL. +// Get potentially employs a more performant strategy than GetWithIndex for retrieving the value. +// If tree.skipFastStorageUpgrade is true, this will work almost the same as GetWithIndex. +func (t *ImmutableTree) Get(key []byte) ([]byte, error) { + if t.root == nil { + return nil, nil + } + + if !t.skipFastStorageUpgrade { + // attempt to get a FastNode directly from db/cache. + // if call fails, fall back to the original IAVL logic in place. + fastNode, err := t.ndb.GetFastNode(key) + if err != nil { + _, result, err := t.root.get(t, key) + return result, err + } + + if fastNode == nil { + // If the tree is of the latest version and fast node is not in the tree + // then the regular node is not in the tree either because fast node + // represents live state. + if t.version == t.ndb.latestVersion { + return nil, nil + } + + _, result, err := t.root.get(t, key) + return result, err + } + + if fastNode.GetVersionLastUpdatedAt() <= t.version { + return fastNode.GetValue(), nil + } + } + + // otherwise skipFastStorageUpgrade is true or + // the cached node was updated later than the current tree. In this case, + // we need to use the regular stategy for reading from the current tree to avoid staleness. + _, result, err := t.root.get(t, key) + return result, err +} + +// GetByIndex gets the key and value at the specified index. +func (t *ImmutableTree) GetByIndex(index int64) (key []byte, value []byte, err error) { + if t.root == nil { + return nil, nil, nil + } + + return t.root.getByIndex(t, index) +} + +// Iterate iterates over all keys of the tree. The keys and values must not be modified, +// since they may point to data stored within IAVL. Returns true if stopped by callback, false otherwise +func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) (bool, error) { + if t.root == nil { + return false, nil + } + + itr, err := t.Iterator(nil, nil, true) + if err != nil { + return false, err + } + defer itr.Close() + + for ; itr.Valid(); itr.Next() { + if fn(itr.Key(), itr.Value()) { + return true, nil + } + } + return false, nil +} + +// Iterator returns an iterator over the immutable tree. +func (t *ImmutableTree) Iterator(start, end []byte, ascending bool) (dbm.Iterator, error) { + if !t.skipFastStorageUpgrade { + isFastCacheEnabled, err := t.IsFastCacheEnabled() + if err != nil { + return nil, err + } + + if isFastCacheEnabled { + return NewFastIterator(start, end, ascending, t.ndb), nil + } + } + return NewIterator(start, end, ascending, t), nil +} + +// IterateRange makes a callback for all nodes with key between start and end non-inclusive. +// If either are nil, then it is open on that side (nil, nil is the same as Iterate). The keys and +// values must not be modified, since they may point to data stored within IAVL. +func (t *ImmutableTree) IterateRange(start, end []byte, ascending bool, fn func(key []byte, value []byte) bool) (stopped bool) { + if t.root == nil { + return false + } + return t.root.traverseInRange(t, start, end, ascending, false, false, func(node *Node) bool { + if node.subtreeHeight == 0 { + return fn(node.key, node.value) + } + return false + }) +} + +// IterateRangeInclusive makes a callback for all nodes with key between start and end inclusive. +// If either are nil, then it is open on that side (nil, nil is the same as Iterate). The keys and +// values must not be modified, since they may point to data stored within IAVL. +func (t *ImmutableTree) IterateRangeInclusive(start, end []byte, ascending bool, fn func(key, value []byte, version int64) bool) (stopped bool) { + if t.root == nil { + return false + } + return t.root.traverseInRange(t, start, end, ascending, true, false, func(node *Node) bool { + if node.subtreeHeight == 0 { + return fn(node.key, node.value, node.nodeKey.version) + } + return false + }) +} + +// IsFastCacheEnabled returns true if fast cache is enabled, false otherwise. +// For fast cache to be enabled, the following 2 conditions must be met: +// 1. The tree is of the latest version. +// 2. The underlying storage has been upgraded to fast cache +func (t *ImmutableTree) IsFastCacheEnabled() (bool, error) { + isLatestTreeVersion, err := t.isLatestTreeVersion() + if err != nil { + return false, err + } + return isLatestTreeVersion && t.ndb.hasUpgradedToFastStorage(), nil +} + +func (t *ImmutableTree) isLatestTreeVersion() (bool, error) { + latestVersion, err := t.ndb.getLatestVersion() + if err != nil { + return false, err + } + return t.version == latestVersion, nil +} + +// Clone creates a clone of the tree. +// Used internally by MutableTree. +func (t *ImmutableTree) clone() *ImmutableTree { + return &ImmutableTree{ + root: t.root, + ndb: t.ndb, + version: t.version, + skipFastStorageUpgrade: t.skipFastStorageUpgrade, + } +} + +// nodeSize is like Size, but includes inner nodes too. +// used only for testing. +func (t *ImmutableTree) nodeSize() int { + return int(t.root.size*2 - 1) +} + +// TraverseStateChanges iterate the range of versions, compare each version to it's predecessor to extract the state changes of it. +// endVersion is exclusive. +func (t *ImmutableTree) TraverseStateChanges(startVersion, endVersion int64, fn func(version int64, changeSet *ChangeSet) error) error { + return t.ndb.traverseStateChanges(startVersion, endVersion, fn) +} diff --git a/iavl/import.go b/iavl/import.go new file mode 100644 index 000000000..282e260ca --- /dev/null +++ b/iavl/import.go @@ -0,0 +1,229 @@ +package iavl + +import ( + "bytes" + "errors" + "fmt" + + db "github.com/cosmos/cosmos-db" +) + +// maxBatchSize is the maximum size of the import batch before flushing it to the database +const maxBatchSize = 10000 + +// ErrNoImport is returned when calling methods on a closed importer +var ErrNoImport = errors.New("no import in progress") + +// Importer imports data into an empty MutableTree. It is created by MutableTree.Import(). Users +// must call Close() when done. +// +// ExportNodes must be imported in the order returned by Exporter, i.e. depth-first post-order (LRN). +// +// Importer is not concurrency-safe, it is the caller's responsibility to ensure the tree is not +// modified while performing an import. +type Importer struct { + tree *MutableTree + version int64 + batch db.Batch + batchSize uint32 + stack []*Node + nonces []uint32 + + // inflightCommit tracks a batch commit, if any. + inflightCommit <-chan error +} + +// newImporter creates a new Importer for an empty MutableTree. +// +// version should correspond to the version that was initially exported. It must be greater than +// or equal to the highest ExportNode version number given. +func newImporter(tree *MutableTree, version int64) (*Importer, error) { + if version < 0 { + return nil, errors.New("imported version cannot be negative") + } + if tree.ndb.latestVersion > 0 { + return nil, fmt.Errorf("found database at version %d, must be 0", tree.ndb.latestVersion) + } + if !tree.IsEmpty() { + return nil, errors.New("tree must be empty") + } + + return &Importer{ + tree: tree, + version: version, + batch: tree.ndb.db.NewBatch(), + stack: make([]*Node, 0, 8), + nonces: make([]uint32, version+1), + }, nil +} + +// writeNode writes the node content to the storage. +func (i *Importer) writeNode(node *Node) error { + node._hash(node.nodeKey.version) + if err := node.validate(); err != nil { + return err + } + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + if err := node.writeBytes(buf); err != nil { + return err + } + + bytesCopy := make([]byte, buf.Len()) + copy(bytesCopy, buf.Bytes()) + + if err := i.batch.Set(i.tree.ndb.nodeKey(node.GetKey()), bytesCopy); err != nil { + return err + } + + i.batchSize++ + if i.batchSize >= maxBatchSize { + // Wait for previous batch. + var err error + if i.inflightCommit != nil { + err = <-i.inflightCommit + i.inflightCommit = nil + } + if err != nil { + return err + } + result := make(chan error) + i.inflightCommit = result + go func(batch db.Batch) { + defer batch.Close() + result <- batch.Write() + }(i.batch) + i.batch = i.tree.ndb.db.NewBatch() + i.batchSize = 0 + } + + return nil +} + +// Close frees all resources. It is safe to call multiple times. Uncommitted nodes may already have +// been flushed to the database, but will not be visible. +func (i *Importer) Close() { + if i.inflightCommit != nil { + <-i.inflightCommit + i.inflightCommit = nil + } + if i.batch != nil { + i.batch.Close() + } + i.batch = nil + i.tree = nil +} + +// Add adds an ExportNode to the import. ExportNodes must be added in the order returned by +// Exporter, i.e. depth-first post-order (LRN). Nodes are periodically flushed to the database, +// but the imported version is not visible until Commit() is called. +func (i *Importer) Add(exportNode *ExportNode) error { + if i.tree == nil { + return ErrNoImport + } + if exportNode == nil { + return errors.New("node cannot be nil") + } + if exportNode.Version > i.version { + return fmt.Errorf("node version %v can't be greater than import version %v", + exportNode.Version, i.version) + } + + node := &Node{ + key: exportNode.Key, + value: exportNode.Value, + subtreeHeight: exportNode.Height, + } + + // We build the tree from the bottom-left up. The stack is used to store unresolved left + // children while constructing right children. When all children are built, the parent can + // be constructed and the resolved children can be discarded from the stack. Using a stack + // ensures that we can handle additional unresolved left children while building a right branch. + // + // We don't modify the stack until we've verified the built node, to avoid leaving the + // importer in an inconsistent state when we return an error. + stackSize := len(i.stack) + if node.subtreeHeight == 0 { + node.size = 1 + } else if stackSize >= 2 && i.stack[stackSize-1].subtreeHeight < node.subtreeHeight && i.stack[stackSize-2].subtreeHeight < node.subtreeHeight { + leftNode := i.stack[stackSize-2] + rightNode := i.stack[stackSize-1] + + node.leftNode = leftNode + node.rightNode = rightNode + node.leftNodeKey = leftNode.GetKey() + node.rightNodeKey = rightNode.GetKey() + node.size = leftNode.size + rightNode.size + + // Update the stack now. + if err := i.writeNode(leftNode); err != nil { + return err + } + if err := i.writeNode(rightNode); err != nil { + return err + } + i.stack = i.stack[:stackSize-2] + + // remove the recursive references to avoid memory leak + leftNode.leftNode = nil + leftNode.rightNode = nil + rightNode.leftNode = nil + rightNode.rightNode = nil + } + i.nonces[exportNode.Version]++ + node.nodeKey = &NodeKey{ + version: exportNode.Version, + // Nonce is 1-indexed, but start at 2 since the root node having a nonce of 1. + nonce: i.nonces[exportNode.Version] + 1, + } + + i.stack = append(i.stack, node) + + return nil +} + +// Commit finalizes the import by flushing any outstanding nodes to the database, making the +// version visible, and updating the tree metadata. It can only be called once, and calls Close() +// internally. +func (i *Importer) Commit() error { + if i.tree == nil { + return ErrNoImport + } + + switch len(i.stack) { + case 0: + if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), []byte{}); err != nil { + return err + } + case 1: + i.stack[0].nodeKey.nonce = 1 + if err := i.writeNode(i.stack[0]); err != nil { + return err + } + if i.stack[0].nodeKey.version < i.version { // it means there is no update in the given version + if err := i.batch.Set(i.tree.ndb.nodeKey(GetRootKey(i.version)), i.tree.ndb.nodeKey(i.stack[0].nodeKey.GetKey())); err != nil { + return err + } + } + default: + return fmt.Errorf("invalid node structure, found stack size %v when committing", + len(i.stack)) + } + + err := i.batch.WriteSync() + if err != nil { + return err + } + i.tree.ndb.resetLatestVersion(i.version) + + _, err = i.tree.LoadVersion(i.version) + if err != nil { + return err + } + + i.Close() + return nil +} diff --git a/iavl/import_test.go b/iavl/import_test.go new file mode 100644 index 000000000..13a925be7 --- /dev/null +++ b/iavl/import_test.go @@ -0,0 +1,274 @@ +package iavl + +import ( + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +func ExampleImporter() { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + _, err := tree.Set([]byte("a"), []byte{1}) + if err != nil { + panic(err) + } + + _, err = tree.Set([]byte("b"), []byte{2}) + if err != nil { + panic(err) + } + _, err = tree.Set([]byte("c"), []byte{3}) + if err != nil { + panic(err) + } + _, version, err := tree.SaveVersion() + if err != nil { + panic(err) + } + + itree, err := tree.GetImmutable(version) + if err != nil { + panic(err) + } + exporter, err := itree.Export() + if err != nil { + panic(err) + } + defer exporter.Close() + exported := []*ExportNode{} + for { + var node *ExportNode + node, err = exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + panic(err) + } + exported = append(exported, node) + } + + newTree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := newTree.Import(version) + if err != nil { + panic(err) + } + defer importer.Close() + for _, node := range exported { + err = importer.Add(node) + if err != nil { + panic(err) + } + } + err = importer.Commit() + if err != nil { + panic(err) + } +} + +func TestImporter_NegativeVersion(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + _, err := tree.Import(-1) + require.Error(t, err) +} + +func TestImporter_NotEmpty(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + +func TestImporter_NotEmptyDatabase(t *testing.T) { + db := dbm.NewMemDB() + + tree := NewMutableTree(db, 0, false, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + +func TestImporter_NotEmptyUnsaved(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + _, err := tree.Set([]byte("a"), []byte{1}) + require.NoError(t, err) + + _, err = tree.Import(1) + require.Error(t, err) +} + +func TestImporter_Add(t *testing.T) { + k := []byte("key") + v := []byte("value") + + testcases := map[string]struct { + node *ExportNode + valid bool + }{ + "nil node": {nil, false}, + "valid": {&ExportNode{Key: k, Value: v, Version: 1, Height: 0}, true}, + "no key": {&ExportNode{Key: nil, Value: v, Version: 1, Height: 0}, false}, + "no value": {&ExportNode{Key: k, Value: nil, Version: 1, Height: 0}, false}, + "version too large": {&ExportNode{Key: k, Value: v, Version: 2, Height: 0}, false}, + "no version": {&ExportNode{Key: k, Value: v, Version: 0, Height: 0}, false}, + // further cases will be handled by Node.validate() + } + for desc, tc := range testcases { + tc := tc // appease scopelint + t.Run(desc, func(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + defer importer.Close() + + err = importer.Add(tc.node) + if tc.valid { + require.NoError(t, err) + } else { + if err == nil { + err = importer.Commit() + } + require.Error(t, err) + } + }) + } +} + +func TestImporter_Add_Closed(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + importer.Close() + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.Error(t, err) + require.Equal(t, ErrNoImport, err) +} + +func TestImporter_Close(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + importer.Close() + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.False(t, has) + + importer.Close() +} + +func TestImporter_Commit(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + err = importer.Commit() + require.NoError(t, err) + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.True(t, has) +} + +func TestImporter_Commit_ForwardVersion(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(2) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + err = importer.Commit() + require.NoError(t, err) + has, err := tree.Has([]byte("key")) + require.NoError(t, err) + require.True(t, has) +} + +func TestImporter_Commit_Closed(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(1) + require.NoError(t, err) + + err = importer.Add(&ExportNode{Key: []byte("key"), Value: []byte("value"), Version: 1, Height: 0}) + require.NoError(t, err) + + importer.Close() + err = importer.Commit() + require.Error(t, err) + require.Equal(t, ErrNoImport, err) +} + +func TestImporter_Commit_Empty(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := tree.Import(3) + require.NoError(t, err) + defer importer.Close() + + err = importer.Commit() + require.NoError(t, err) + assert.EqualValues(t, 3, tree.Version()) +} + +func BenchmarkImport(b *testing.B) { + benchmarkImport(b, 4096) +} + +func BenchmarkImportBatch(b *testing.B) { + benchmarkImport(b, maxBatchSize*10) +} + +func benchmarkImport(b *testing.B, nodes int) { + b.StopTimer() + tree := setupExportTreeSized(b, nodes) + exported := make([]*ExportNode, 0, nodes) + exporter, err := tree.Export() + require.NoError(b, err) + for { + item, err := exporter.Next() + if err == ErrorExportDone { + break + } else if err != nil { + b.Error(err) + } + exported = append(exported, item) + } + exporter.Close() + b.StartTimer() + + for n := 0; n < b.N; n++ { + newTree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + importer, err := newTree.Import(tree.Version()) + require.NoError(b, err) + for _, item := range exported { + err = importer.Add(item) + if err != nil { + b.Error(err) + } + } + err = importer.Commit() + require.NoError(b, err) + } +} diff --git a/iavl/internal/bytes/bytes.go b/iavl/internal/bytes/bytes.go new file mode 100644 index 000000000..23c042196 --- /dev/null +++ b/iavl/internal/bytes/bytes.go @@ -0,0 +1,94 @@ +package common + +import ( + "encoding/hex" + "fmt" + "strings" +) + +// The main purpose of HexBytes is to enable HEX-encoding for json/encoding. +type HexBytes []byte + +// Marshal needed for protobuf compatibility +func (bz HexBytes) Marshal() ([]byte, error) { + return bz, nil +} + +// Unmarshal needed for protobuf compatibility +func (bz *HexBytes) Unmarshal(data []byte) error { + *bz = data + return nil +} + +// This is the point of Bytes. +func (bz HexBytes) MarshalJSON() ([]byte, error) { + s := strings.ToUpper(hex.EncodeToString(bz)) + jbz := make([]byte, len(s)+2) + jbz[0] = '"' + copy(jbz[1:], s) + jbz[len(jbz)-1] = '"' + return jbz, nil +} + +// This is the point of Bytes. +func (bz *HexBytes) UnmarshalJSON(data []byte) error { + if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' { + return fmt.Errorf("invalid hex string: %s", data) + } + data = data[1 : len(data)-1] + dest := make([]byte, hex.DecodedLen(len(data))) + _, err := hex.Decode(dest, data) + if err != nil { + return err + } + *bz = dest + return nil +} + +// Allow it to fulfill various interfaces in light-client, etc... +func (bz HexBytes) Bytes() []byte { + return bz +} + +func (bz HexBytes) String() string { + return strings.ToUpper(hex.EncodeToString(bz)) +} + +func (bz HexBytes) Format(s fmt.State, verb rune) { + switch verb { + case 'p': + s.Write([]byte(fmt.Sprintf("%p", bz))) + default: + s.Write([]byte(fmt.Sprintf("%X", []byte(bz)))) + } +} + +// Returns a copy of the given byte slice. +func Cp(bz []byte) (ret []byte) { + ret = make([]byte, len(bz)) + copy(ret, bz) + return ret +} + +// Returns a slice of the same length (big endian) +// except incremented by one. +// Returns nil on overflow (e.g. if bz bytes are all 0xFF) +// CONTRACT: len(bz) > 0 +func CpIncr(bz []byte) (ret []byte) { + if len(bz) == 0 { + panic("cpIncr expects non-zero bz length") + } + ret = Cp(bz) + for i := len(bz) - 1; i >= 0; i-- { + if ret[i] < byte(0xFF) { + ret[i]++ + return + } + ret[i] = byte(0x00) + if i == 0 { + // Overflow + return nil + } + } + return nil +} diff --git a/iavl/internal/bytes/bytes_test.go b/iavl/internal/bytes/bytes_test.go new file mode 100644 index 000000000..f7636d0a4 --- /dev/null +++ b/iavl/internal/bytes/bytes_test.go @@ -0,0 +1,65 @@ +// nolint: scopelint +package common + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// This is a trivial test for protobuf compatibility. +func TestMarshal(t *testing.T) { + bz := []byte("hello world") + dataB := HexBytes(bz) + bz2, err := dataB.Marshal() + assert.Nil(t, err) + assert.Equal(t, bz, bz2) + + var dataB2 HexBytes + err = (&dataB2).Unmarshal(bz) + assert.Nil(t, err) + assert.Equal(t, dataB, dataB2) +} + +// Test that the hex encoding works. +func TestJSONMarshal(t *testing.T) { + type TestStruct struct { + B1 []byte + B2 HexBytes + } + + cases := []struct { + input []byte + expected string + }{ + {[]byte(``), `{"B1":"","B2":""}`}, + {[]byte(`a`), `{"B1":"YQ==","B2":"61"}`}, + {[]byte(`abc`), `{"B1":"YWJj","B2":"616263"}`}, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) { + ts := TestStruct{B1: tc.input, B2: tc.input} + + // Test that it marshals correctly to JSON. + jsonBytes, err := json.Marshal(ts) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, string(jsonBytes), tc.expected) + + // TODO do fuzz testing to ensure that unmarshal fails + + // Test that unmarshaling works correctly. + ts2 := TestStruct{} + err = json.Unmarshal(jsonBytes, &ts2) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, ts2.B1, tc.input) + assert.Equal(t, ts2.B2, HexBytes(tc.input)) + }) + } +} diff --git a/iavl/internal/bytes/string.go b/iavl/internal/bytes/string.go new file mode 100644 index 000000000..698edbed8 --- /dev/null +++ b/iavl/internal/bytes/string.go @@ -0,0 +1,25 @@ +package common + +import ( + "unsafe" +) + +// UnsafeStrToBytes uses unsafe to convert string into byte array. Returned bytes +// must not be altered after this function is called as it will cause a segmentation fault. +func UnsafeStrToBytes(s string) []byte { + if len(s) == 0 { + return nil + } + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +// UnsafeBytesToStr is meant to make a zero allocation conversion +// from []byte -> string to speed up operations, it is not meant +// to be used generally, but for a specific pattern to delete keys +// from a map. +func UnsafeBytesToStr(b []byte) string { + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) +} diff --git a/iavl/internal/bytes/string_test.go b/iavl/internal/bytes/string_test.go new file mode 100644 index 000000000..cc5bbb78f --- /dev/null +++ b/iavl/internal/bytes/string_test.go @@ -0,0 +1,54 @@ +package common + +import ( + "runtime" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +func TestStringSuite(t *testing.T) { + suite.Run(t, new(StringSuite)) +} + +type StringSuite struct{ suite.Suite } + +func unsafeConvertStr() []byte { + return UnsafeStrToBytes("abc") +} + +func (s *StringSuite) TestUnsafeStrToBytes() { + // we convert in other function to trigger GC. We want to check that + // the underlying array in []bytes is accessible after GC will finish swapping. + for i := 0; i < 5; i++ { + b := unsafeConvertStr() + runtime.GC() + <-time.NewTimer(2 * time.Millisecond).C + b2 := append(b, 'd') //nolint:gocritic + s.Equal("abc", string(b)) + s.Equal("abcd", string(b2)) + } +} + +func unsafeConvertBytes() string { + return UnsafeBytesToStr([]byte("abc")) +} + +func (s *StringSuite) TestUnsafeBytesToStr() { + // we convert in other function to trigger GC. We want to check that + // the underlying array in []bytes is accessible after GC will finish swapping. + for i := 0; i < 5; i++ { + str := unsafeConvertBytes() + runtime.GC() + <-time.NewTimer(2 * time.Millisecond).C + s.Equal("abc", str) + } +} + +func BenchmarkUnsafeStrToBytes(b *testing.B) { + for i := 0; i < b.N; i++ { + UnsafeStrToBytes(strconv.Itoa(i)) + } +} diff --git a/iavl/internal/color/colors.go b/iavl/internal/color/colors.go new file mode 100644 index 000000000..26193150f --- /dev/null +++ b/iavl/internal/color/colors.go @@ -0,0 +1,67 @@ +// TODO: Can we delete this package. Honestly pretty insane this is in our database. +package color + +import ( + "fmt" + "os" + "strings" +) + +const ( + ANSIReset = "\x1b[0m" + ANSIBright = "\x1b[1m" + + ANSIFgGreen = "\x1b[32m" + ANSIFgBlue = "\x1b[34m" + ANSIFgCyan = "\x1b[36m" +) + +// color the string s with color 'color' +// unless s is already colored +func treat(s string, color string) string { + if len(s) > 2 && s[:2] == "\x1b[" { + return s + } + return color + s + ANSIReset +} + +func treatAll(color string, args ...interface{}) string { + parts := make([]string, 0, len(args)) + for _, arg := range args { + parts = append(parts, treat(fmt.Sprintf("%v", arg), color)) + } + return strings.Join(parts, "") +} + +func Green(args ...interface{}) string { + return treatAll(ANSIFgGreen, args...) +} + +func Blue(args ...interface{}) string { + return treatAll(ANSIFgBlue, args...) +} + +func Cyan(args ...interface{}) string { + return treatAll(ANSIFgCyan, args...) +} + +// ColoredBytes takes in the byte that you would like to show as a string and byte +// and will display them in a human readable format. +// If the environment variable TENDERMINT_IAVL_COLORS_ON is set to a non-empty string then different colors will be used for bytes and strings. +func ColoredBytes(data []byte, textColor, bytesColor func(...interface{}) string) string { + colors := os.Getenv("TENDERMINT_IAVL_COLORS_ON") + if colors == "" { + for _, b := range data { + return string(b) + } + } + s := "" + for _, b := range data { + if 0x21 <= b && b < 0x7F { + s += textColor(string(b)) + } else { + s += bytesColor(fmt.Sprintf("%02X", b)) + } + } + return s +} diff --git a/iavl/internal/encoding/bench_test.go b/iavl/internal/encoding/bench_test.go new file mode 100644 index 000000000..03a2f320b --- /dev/null +++ b/iavl/internal/encoding/bench_test.go @@ -0,0 +1,59 @@ +package encoding + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "testing" +) + +var encValues = []int64{ + -1, -100, -1 << 32, + 0, 1, 100, 1 << 32, + -1 << 52, 1 << 52, 17, + 19, 28, 37, 388888888, + -99999999999, 99999999999, + math.MaxInt64, math.MinInt64, +} + +// This tests that the results from directly invoking binary.PutVarint match +// exactly those that we get from invoking EncodeVarint and its internals. +func TestEncodeVarintParity(t *testing.T) { + buf := new(bytes.Buffer) + var board [binary.MaxVarintLen64]byte + + for _, val := range encValues { + val := val + name := fmt.Sprintf("%d", val) + + buf.Reset() + t.Run(name, func(t *testing.T) { + if err := EncodeVarint(buf, val); err != nil { + t.Fatal(err) + } + + n := binary.PutVarint(board[:], val) + got := buf.Bytes() + want := board[:n] + if !bytes.Equal(got, want) { + t.Fatalf("Result mismatch\n\tGot: %d\n\tWant: %d", got, want) + } + }) + } +} + +func BenchmarkEncodeVarint(b *testing.B) { + buf := new(bytes.Buffer) + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, val := range encValues { + if err := EncodeVarint(buf, val); err != nil { + b.Fatal(err) + } + buf.Reset() + } + } +} diff --git a/iavl/internal/encoding/encoding.go b/iavl/internal/encoding/encoding.go new file mode 100644 index 000000000..6e390c36c --- /dev/null +++ b/iavl/internal/encoding/encoding.go @@ -0,0 +1,207 @@ +package encoding + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "math/bits" + "sync" +) + +var bufPool = &sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var varintPool = &sync.Pool{ + New: func() interface{} { + return &[binary.MaxVarintLen64]byte{} + }, +} + +var uvarintPool = &sync.Pool{ + New: func() interface{} { + return &[binary.MaxVarintLen64]byte{} + }, +} + +// decodeBytes decodes a varint length-prefixed byte slice, returning it along with the number +// of input bytes read. +// Assumes bz will not be mutated. +func DecodeBytes(bz []byte) ([]byte, int, error) { + s, n, err := DecodeUvarint(bz) + if err != nil { + return nil, n, err + } + // Make sure size doesn't overflow. ^uint(0) >> 1 will help determine the + // max int value variably on 32-bit and 64-bit machines. We also doublecheck + // that size is positive. + size := int(s) + if s >= uint64(^uint(0)>>1) || size < 0 { + return nil, n, fmt.Errorf("invalid out of range length %v decoding []byte", s) + } + // Make sure end index doesn't overflow. We know n>0 from decodeUvarint(). + end := n + size + if end < n { + return nil, n, fmt.Errorf("invalid out of range length %v decoding []byte", size) + } + // Make sure the end index is within bounds. + if len(bz) < end { + return nil, n, fmt.Errorf("insufficient bytes decoding []byte of length %v", size) + } + return bz[n:end], end, nil +} + +// decodeUvarint decodes a varint-encoded unsigned integer from a byte slice, returning it and the +// number of bytes decoded. +func DecodeUvarint(bz []byte) (uint64, int, error) { + u, n := binary.Uvarint(bz) + if n == 0 { + // buf too small + return u, n, errors.New("buffer too small") + } else if n < 0 { + // value larger than 64 bits (overflow) + // and -n is the number of bytes read + n = -n + return u, n, errors.New("EOF decoding uvarint") + } + return u, n, nil +} + +// decodeVarint decodes a varint-encoded integer from a byte slice, returning it and the number of +// bytes decoded. +func DecodeVarint(bz []byte) (int64, int, error) { + i, n := binary.Varint(bz) + if n == 0 { + return i, n, errors.New("buffer too small") + } else if n < 0 { + // value larger than 64 bits (overflow) + // and -n is the number of bytes read + n = -n + return i, n, errors.New("EOF decoding varint") + } + return i, n, nil +} + +// EncodeBytes writes a varint length-prefixed byte slice to the writer. +func EncodeBytes(w io.Writer, bz []byte) error { + err := EncodeUvarint(w, uint64(len(bz))) + if err != nil { + return err + } + _, err = w.Write(bz) + return err +} + +var hashLenBz []byte + +func init() { + hashLenBz = make([]byte, 1) + binary.PutUvarint(hashLenBz, 32) +} + +// Encode 32 byte long hash +func Encode32BytesHash(w io.Writer, bz []byte) error { + _, err := w.Write(hashLenBz) + if err != nil { + return err + } + _, err = w.Write(bz) + return err +} + +// encodeBytesSlice length-prefixes the byte slice and returns it. +func EncodeBytesSlice(bz []byte) ([]byte, error) { + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + err := EncodeBytes(buf, bz) + + bytesCopy := make([]byte, buf.Len()) + copy(bytesCopy, buf.Bytes()) + + return bytesCopy, err +} + +// encodeBytesSize returns the byte size of the given slice including length-prefixing. +func EncodeBytesSize(bz []byte) int { + return EncodeUvarintSize(uint64(len(bz))) + len(bz) +} + +// EncodeUvarint writes a varint-encoded unsigned integer to an io.Writer. +func EncodeUvarint(w io.Writer, u uint64) error { + // See comment in encodeVarint + buf := uvarintPool.Get().(*[binary.MaxVarintLen64]byte) + + n := binary.PutUvarint(buf[:], u) + _, err := w.Write(buf[0:n]) + + uvarintPool.Put(buf) + + return err +} + +// EncodeUvarintSize returns the byte size of the given integer as a varint. +func EncodeUvarintSize(u uint64) int { + if u == 0 { + return 1 + } + return (bits.Len64(u) + 6) / 7 +} + +// EncodeVarint writes a varint-encoded integer to an io.Writer. +func EncodeVarint(w io.Writer, i int64) error { + if bw, ok := w.(io.ByteWriter); ok { + return fVarintEncode(bw, i) + } + + // Use a pool here to reduce allocations. + // + // Though this allocates just 10 bytes on the stack, doing allocation for every calls + // cost us a huge memory. The profiling show that using pool save us ~30% memory. + // + // Since when we don't have concurrent access to the pool, the speed will nearly identical. + // If we need to support concurrent access, we can accept a *[binary.MaxVarintLen64]byte as + // input, so the caller can allocate just one and pass the same array pointer to each call. + buf := varintPool.Get().(*[binary.MaxVarintLen64]byte) + + n := binary.PutVarint(buf[:], i) + _, err := w.Write(buf[0:n]) + + varintPool.Put(buf) + + return err +} + +func fVarintEncode(bw io.ByteWriter, x int64) error { + // Firstly convert it into a uvarint + ux := uint64(x) << 1 + if x < 0 { + ux = ^ux + } + for ux >= 0x80 { // While there are 7 or more bits in the value, keep going + // Convert it into a byte then toggle the + // 7th bit to indicate that more bytes coming. + // byte(x & 0x7f) is redundant but useful for illustrative + // purposes when translating to other languages + if err := bw.WriteByte(byte(ux&0x7f) | 0x80); err != nil { + return err + } + ux >>= 7 + } + + return bw.WriteByte(byte(ux & 0x7f)) +} + +// EncodeVarintSize returns the byte size of the given integer as a varint. +func EncodeVarintSize(i int64) int { + ux := uint64(i) << 1 + if i < 0 { + ux = ^ux + } + return EncodeUvarintSize(ux) +} diff --git a/iavl/internal/encoding/encoding_test.go b/iavl/internal/encoding/encoding_test.go new file mode 100644 index 000000000..36c402f3e --- /dev/null +++ b/iavl/internal/encoding/encoding_test.go @@ -0,0 +1,88 @@ +package encoding + +import ( + "encoding/binary" + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDecodeBytes(t *testing.T) { + bz := []byte{0, 1, 2, 3, 4, 5, 6, 7} + testcases := map[string]struct { + bz []byte + lengthPrefix uint64 + expect []byte + expectErr bool + }{ + "full": {bz, 8, bz, false}, + "empty": {bz, 0, []byte{}, false}, + "partial": {bz, 3, []byte{0, 1, 2}, false}, + "out of bounds": {bz, 9, nil, true}, + "empty input": {[]byte{}, 0, []byte{}, false}, + "empty input out of bounds": {[]byte{}, 1, nil, true}, + + // The following will always fail, since the byte slice is only 8 bytes, + // but we're making sure they don't panic due to overflow issues. See: + // https://github.com/cosmos/iavl/issues/339 + "max int32": {bz, uint64(math.MaxInt32), nil, true}, + "max int32 -1": {bz, uint64(math.MaxInt32) - 1, nil, true}, + "max int32 -10": {bz, uint64(math.MaxInt32) - 10, nil, true}, + "max int32 +1": {bz, uint64(math.MaxInt32) + 1, nil, true}, + "max int32 +10": {bz, uint64(math.MaxInt32) + 10, nil, true}, + + "max int32*2": {bz, uint64(math.MaxInt32) * 2, nil, true}, + "max int32*2 -1": {bz, uint64(math.MaxInt32)*2 - 1, nil, true}, + "max int32*2 -10": {bz, uint64(math.MaxInt32)*2 - 10, nil, true}, + "max int32*2 +1": {bz, uint64(math.MaxInt32)*2 + 1, nil, true}, + "max int32*2 +10": {bz, uint64(math.MaxInt32)*2 + 10, nil, true}, + + "max uint32": {bz, uint64(math.MaxUint32), nil, true}, + "max uint32 -1": {bz, uint64(math.MaxUint32) - 1, nil, true}, + "max uint32 -10": {bz, uint64(math.MaxUint32) - 10, nil, true}, + "max uint32 +1": {bz, uint64(math.MaxUint32) + 1, nil, true}, + "max uint32 +10": {bz, uint64(math.MaxUint32) + 10, nil, true}, + + "max uint32*2": {bz, uint64(math.MaxUint32) * 2, nil, true}, + "max uint32*2 -1": {bz, uint64(math.MaxUint32)*2 - 1, nil, true}, + "max uint32*2 -10": {bz, uint64(math.MaxUint32)*2 - 10, nil, true}, + "max uint32*2 +1": {bz, uint64(math.MaxUint32)*2 + 1, nil, true}, + "max uint32*2 +10": {bz, uint64(math.MaxUint32)*2 + 10, nil, true}, + + "max int64": {bz, uint64(math.MaxInt64), nil, true}, + "max int64 -1": {bz, uint64(math.MaxInt64) - 1, nil, true}, + "max int64 -10": {bz, uint64(math.MaxInt64) - 10, nil, true}, + "max int64 +1": {bz, uint64(math.MaxInt64) + 1, nil, true}, + "max int64 +10": {bz, uint64(math.MaxInt64) + 10, nil, true}, + + "max uint64": {bz, uint64(math.MaxUint64), nil, true}, + "max uint64 -1": {bz, uint64(math.MaxUint64) - 1, nil, true}, + "max uint64 -10": {bz, uint64(math.MaxUint64) - 10, nil, true}, + } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + // Generate an input slice. + buf := make([]byte, binary.MaxVarintLen64) + varintBytes := binary.PutUvarint(buf, tc.lengthPrefix) + buf = append(buf[:varintBytes], tc.bz...) + + // Attempt to decode it. + b, n, err := DecodeBytes(buf) + if tc.expectErr { + require.Error(t, err) + require.Equal(t, varintBytes, n) + } else { + require.NoError(t, err) + require.Equal(t, uint64(n), uint64(varintBytes)+tc.lengthPrefix) + require.Equal(t, tc.bz[:tc.lengthPrefix], b) + } + }) + } +} + +func TestDecodeBytes_invalidVarint(t *testing.T) { + _, _, err := DecodeBytes([]byte{0xff}) + require.Error(t, err) +} diff --git a/iavl/internal/rand/random.go b/iavl/internal/rand/random.go new file mode 100644 index 000000000..9fc3b1494 --- /dev/null +++ b/iavl/internal/rand/random.go @@ -0,0 +1,244 @@ +package common + +import ( + crand "crypto/rand" + mrand "math/rand" + "sync" + "time" +) + +const ( + strChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" // 62 characters +) + +// Rand is a prng, that is seeded with OS randomness. +// The OS randomness is obtained from crypto/rand, however none of the provided +// methods are suitable for cryptographic usage. +// They all utilize math/rand's prng internally. +// +// All of the methods here are suitable for concurrent use. +// This is achieved by using a mutex lock on all of the provided methods. +type Rand struct { + sync.Mutex + rand *mrand.Rand +} + +var grand *Rand + +func init() { + grand = NewRand() + grand.init() +} + +func NewRand() *Rand { + rand := &Rand{} + rand.init() + return rand +} + +func (r *Rand) init() { + bz := cRandBytes(8) + var seed uint64 + for i := 0; i < 8; i++ { + seed |= uint64(bz[i]) + seed <<= 8 + } + r.reset(int64(seed)) +} + +func (r *Rand) reset(seed int64) { + r.rand = mrand.New(mrand.NewSource(seed)) +} + +//---------------------------------------- +// Global functions + +func Seed(seed int64) { + grand.Seed(seed) +} + +func RandStr(length int) string { + return grand.Str(length) +} + +func RandInt() int { + return grand.Int() +} + +func RandInt31() int32 { + return grand.Int31() +} + +func RandBytes(n int) []byte { + return grand.Bytes(n) +} + +func RandPerm(n int) []int { + return grand.Perm(n) +} + +//---------------------------------------- +// Rand methods + +func (r *Rand) Seed(seed int64) { + r.Lock() + r.reset(seed) + r.Unlock() +} + +// Str constructs a random alphanumeric string of given length. +func (r *Rand) Str(length int) string { + chars := []byte{} +MAIN_LOOP: + for { + val := r.Int63() + for i := 0; i < 10; i++ { + v := int(val & 0x3f) // rightmost 6 bits + if v >= 62 { // only 62 characters in strChars + val >>= 6 + continue + } + chars = append(chars, strChars[v]) + if len(chars) == length { + break MAIN_LOOP + } + val >>= 6 + } + } + + return string(chars) +} + +func (r *Rand) Uint16() uint16 { + return uint16(r.Uint32() & (1<<16 - 1)) +} + +func (r *Rand) Uint32() uint32 { + r.Lock() + u32 := r.rand.Uint32() + r.Unlock() + return u32 +} + +func (r *Rand) Uint64() uint64 { + return uint64(r.Uint32())<<32 + uint64(r.Uint32()) +} + +func (r *Rand) Uint() uint { + r.Lock() + i := r.rand.Int() + r.Unlock() + return uint(i) +} + +func (r *Rand) Int16() int16 { + return int16(r.Uint32() & (1<<16 - 1)) +} + +func (r *Rand) Int32() int32 { + return int32(r.Uint32()) +} + +func (r *Rand) Int64() int64 { + return int64(r.Uint64()) +} + +func (r *Rand) Int() int { + r.Lock() + defer r.Unlock() + i := r.rand.Int() + return i +} + +func (r *Rand) Int31() int32 { + r.Lock() + defer r.Unlock() + i31 := r.rand.Int31() + return i31 +} + +func (r *Rand) Int31n(n int32) int32 { + r.Lock() + defer r.Unlock() + i31n := r.rand.Int31n(n) + return i31n +} + +func (r *Rand) Int63() int64 { + r.Lock() + defer r.Unlock() + i63 := r.rand.Int63() + return i63 +} + +func (r *Rand) Int63n(n int64) int64 { + r.Lock() + defer r.Unlock() + i63n := r.rand.Int63n(n) + return i63n +} + +func (r *Rand) Float32() float32 { + r.Lock() + defer r.Unlock() + f32 := r.rand.Float32() + return f32 +} + +func (r *Rand) Float64() float64 { + r.Lock() + defer r.Unlock() + f64 := r.rand.Float64() + return f64 +} + +func (r *Rand) Time() time.Time { + return time.Unix(int64(r.Uint64()), 0) +} + +// Bytes returns n random bytes generated from the internal +// prng. +func (r *Rand) Bytes(n int) []byte { + // cRandBytes isn't guaranteed to be fast so instead + // use random bytes generated from the internal PRNG + bs := make([]byte, n) + for i := 0; i < len(bs); i++ { + bs[i] = byte(r.Int() & 0xFF) + } + return bs +} + +// Intn returns, as an int, a uniform pseudo-random number in the range [0, n). +// It panics if n <= 0. +func (r *Rand) Intn(n int) int { + r.Lock() + defer r.Unlock() + i := r.rand.Intn(n) + return i +} + +// Bool returns a uniformly random boolean +func (r *Rand) Bool() bool { + // See https://github.com/golang/go/issues/23804#issuecomment-365370418 + // for reasoning behind computing like this + return r.Int63()%2 == 0 +} + +// Perm returns a pseudo-random permutation of n integers in [0, n). +func (r *Rand) Perm(n int) []int { + r.Lock() + defer r.Unlock() + perm := r.rand.Perm(n) + return perm +} + +// NOTE: This relies on the os's random number generator. +// For real security, we should salt that with some seed. +func cRandBytes(numBytes int) []byte { + b := make([]byte, numBytes) + _, err := crand.Read(b) + if err != nil { + panic(err) + } + return b +} diff --git a/iavl/internal/rand/random_test.go b/iavl/internal/rand/random_test.go new file mode 100644 index 000000000..793557f6f --- /dev/null +++ b/iavl/internal/rand/random_test.go @@ -0,0 +1,83 @@ +package common + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRandStr(t *testing.T) { + l := 243 + s := RandStr(l) + assert.Equal(t, l, len(s)) +} + +func TestRandBytes(t *testing.T) { + l := 243 + b := RandBytes(l) + assert.Equal(t, l, len(b)) +} + +// Test to make sure that we never call math.rand(). +// We do this by ensuring that outputs are deterministic. +func TestDeterminism(t *testing.T) { + var firstOutput string + + for i := 0; i < 100; i++ { + output := testThemAll() + if i == 0 { + firstOutput = output + } else if firstOutput != output { + t.Errorf("Run #%d's output was different from first run.\nfirst: %v\nlast: %v", + i, firstOutput, output) + } + } +} + +func testThemAll() string { + // Such determinism. + grand.reset(1) + + // Use it. + out := new(bytes.Buffer) + perm := RandPerm(10) + blob, _ := json.Marshal(perm) + fmt.Fprintf(out, "perm: %s\n", blob) + fmt.Fprintf(out, "randInt: %d\n", RandInt()) + fmt.Fprintf(out, "randInt31: %d\n", RandInt31()) + return out.String() +} + +func BenchmarkRandBytes10B(b *testing.B) { + benchmarkRandBytes(b, 10) +} + +func BenchmarkRandBytes100B(b *testing.B) { + benchmarkRandBytes(b, 100) +} + +func BenchmarkRandBytes1KiB(b *testing.B) { + benchmarkRandBytes(b, 1024) +} + +func BenchmarkRandBytes10KiB(b *testing.B) { + benchmarkRandBytes(b, 10*1024) +} + +func BenchmarkRandBytes100KiB(b *testing.B) { + benchmarkRandBytes(b, 100*1024) +} + +func BenchmarkRandBytes1MiB(b *testing.B) { + benchmarkRandBytes(b, 1024*1024) +} + +func benchmarkRandBytes(b *testing.B, n int) { + for i := 0; i < b.N; i++ { + _ = RandBytes(n) + } + b.ReportAllocs() +} diff --git a/iavl/iterator.go b/iavl/iterator.go new file mode 100644 index 000000000..131a2bdda --- /dev/null +++ b/iavl/iterator.go @@ -0,0 +1,335 @@ +package iavl + +// NOTE: This file favors int64 as opposed to int for size/counts. +// The Tree on the other hand favors int. This is intentional. + +import ( + "bytes" + "errors" + + dbm "github.com/cosmos/cosmos-db" +) + +type traversal struct { + tree *ImmutableTree + start, end []byte // iteration domain + ascending bool // ascending traversal + inclusive bool // end key inclusiveness + post bool // postorder traversal + delayedNodes *delayedNodes // delayed nodes to be traversed +} + +var errIteratorNilTreeGiven = errors.New("iterator must be created with an immutable tree but the tree was nil") + +func (node *Node) newTraversal(tree *ImmutableTree, start, end []byte, ascending bool, inclusive bool, post bool) *traversal { + return &traversal{ + tree: tree, + start: start, + end: end, + ascending: ascending, + inclusive: inclusive, + post: post, + delayedNodes: &delayedNodes{{node, true}}, // set initial traverse to the node + } +} + +// delayedNode represents the delayed iteration on the nodes. +// When delayed is set to true, the delayedNode should be expanded, and their +// children should be traversed. When delayed is set to false, the delayedNode is +// already have expanded, and it could be immediately returned. +type delayedNode struct { + node *Node + delayed bool +} + +type delayedNodes []delayedNode + +func (nodes *delayedNodes) pop() (*Node, bool) { + node := (*nodes)[len(*nodes)-1] + *nodes = (*nodes)[:len(*nodes)-1] + return node.node, node.delayed +} + +func (nodes *delayedNodes) push(node *Node, delayed bool) { + *nodes = append(*nodes, delayedNode{node, delayed}) +} + +func (nodes *delayedNodes) length() int { + return len(*nodes) +} + +// `traversal` returns the delayed execution of recursive traversal on a tree. +// +// `traversal` will traverse the tree in a depth-first manner. To handle locating +// the next element, and to handle unwinding, the traversal maintains its future +// iteration under `delayedNodes`. At each call of `next()`, it will retrieve the +// next element from the `delayedNodes` and acts accordingly. The `next()` itself +// defines how to unwind the delayed nodes stack. The caller can either call the +// next traversal to proceed, or simply discard the `traversal` struct to stop iteration. +// +// At the each step of `next`, the `delayedNodes` can have one of the three states: +// 1. It has length of 0, meaning that their is no more traversable nodes. +// 2. It has length of 1, meaning that the traverse is being started from the initial node. +// 3. It has length of 2>=, meaning that there are delayed nodes to be traversed. +// +// When the `delayedNodes` are not empty, `next` retrieves the first `delayedNode` and initially check: +// 1. If it is not an delayed node (node.delayed == false) it immediately returns it. +// +// A. If the `node` is a branch node: +// 1. If the traversal is postorder, then append the current node to the t.delayedNodes, +// with `delayed` set to false. This makes the current node returned *after* all the children +// are traversed, without being expanded. +// 2. Append the traversable children nodes into the `delayedNodes`, with `delayed` set to true. This +// makes the children nodes to be traversed, and expanded with their respective children. +// 3. If the traversal is preorder, (with the children to be traversed already pushed to the +// `delayedNodes`), returns the current node. +// 4. Call `traversal.next()` to further traverse through the `delayedNodes`. +// +// B. If the `node` is a leaf node, it will be returned without expand, by the following process: +// 1. If the traversal is postorder, the current node will be append to the `delayedNodes` with `delayed` +// set to false, and immediately returned at the subsequent call of `traversal.next()` at the last line. +// 2. If the traversal is preorder, the current node will be returned. +func (t *traversal) next() (*Node, error) { + // End of traversal. + if t.delayedNodes.length() == 0 { + return nil, nil + } + + node, delayed := t.delayedNodes.pop() + + // Already expanded, immediately return. + if !delayed || node == nil { + return node, nil + } + + afterStart := t.start == nil || bytes.Compare(t.start, node.key) < 0 + startOrAfter := afterStart || bytes.Equal(t.start, node.key) + beforeEnd := t.end == nil || bytes.Compare(node.key, t.end) < 0 + if t.inclusive { + beforeEnd = beforeEnd || bytes.Equal(node.key, t.end) + } + + // case of postorder. A-1 and B-1 + // Recursively process left sub-tree, then right-subtree, then node itself. + if t.post && (!node.isLeaf() || (startOrAfter && beforeEnd)) { + t.delayedNodes.push(node, false) + } + + // case of branch node, traversing children. A-2. + if !node.isLeaf() { + // if node is a branch node and the order is ascending, + // We traverse through the left subtree, then the right subtree. + if t.ascending { + if beforeEnd { + // push the delayed traversal for the right nodes, + rightNode, err := node.getRightNode(t.tree) + if err != nil { + return nil, err + } + t.delayedNodes.push(rightNode, true) + } + if afterStart { + // push the delayed traversal for the left nodes, + leftNode, err := node.getLeftNode(t.tree) + if err != nil { + return nil, err + } + t.delayedNodes.push(leftNode, true) + } + } else { + // if node is a branch node and the order is not ascending + // We traverse through the right subtree, then the left subtree. + if afterStart { + // push the delayed traversal for the left nodes, + leftNode, err := node.getLeftNode(t.tree) + if err != nil { + return nil, err + } + t.delayedNodes.push(leftNode, true) + } + if beforeEnd { + // push the delayed traversal for the right nodes, + rightNode, err := node.getRightNode(t.tree) + if err != nil { + return nil, err + } + t.delayedNodes.push(rightNode, true) + } + } + } + + // case of preorder traversal. A-3 and B-2. + // Process root then (recursively) processing left child, then process right child + if !t.post && (!node.isLeaf() || (startOrAfter && beforeEnd)) { + return node, nil + } + + // Keep traversing and expanding the remaning delayed nodes. A-4. + return t.next() +} + +// Iterator is a dbm.Iterator for ImmutableTree +type Iterator struct { + start, end []byte + + key, value []byte + + valid bool + + err error + + t *traversal +} + +var _ dbm.Iterator = (*Iterator)(nil) + +// Returns a new iterator over the immutable tree. If the tree is nil, the iterator will be invalid. +func NewIterator(start, end []byte, ascending bool, tree *ImmutableTree) dbm.Iterator { + iter := &Iterator{ + start: start, + end: end, + } + + if tree == nil { + iter.err = errIteratorNilTreeGiven + } else { + iter.valid = true + iter.t = tree.root.newTraversal(tree, start, end, ascending, false, false) + // Move iterator before the first element + iter.Next() + } + return iter +} + +// Domain implements dbm.Iterator. +func (iter *Iterator) Domain() ([]byte, []byte) { + return iter.start, iter.end +} + +// Valid implements dbm.Iterator. +func (iter *Iterator) Valid() bool { + return iter.valid +} + +// Key implements dbm.Iterator +func (iter *Iterator) Key() []byte { + return iter.key +} + +// Value implements dbm.Iterator +func (iter *Iterator) Value() []byte { + return iter.value +} + +// Next implements dbm.Iterator +func (iter *Iterator) Next() { + if iter.t == nil { + return + } + + node, err := iter.t.next() + // TODO: double-check if this error is correctly handled. + if node == nil || err != nil { + iter.t = nil + iter.valid = false + return + } + + if node.subtreeHeight == 0 { + iter.key, iter.value = node.key, node.value + return + } + + iter.Next() +} + +// Close implements dbm.Iterator +func (iter *Iterator) Close() error { + iter.t = nil + iter.valid = false + return iter.err +} + +// Error implements dbm.Iterator +func (iter *Iterator) Error() error { + return iter.err +} + +// IsFast returnts true if iterator uses fast strategy +func (iter *Iterator) IsFast() bool { + return false +} + +// NodeIterator is an iterator for nodeDB to traverse a tree in depth-first, preorder manner. +type NodeIterator struct { + nodesToVisit []*Node + ndb *nodeDB + err error +} + +// NewNodeIterator returns a new NodeIterator to traverse the tree of the root node. +func NewNodeIterator(rootKey []byte, ndb *nodeDB) (*NodeIterator, error) { + if len(rootKey) == 0 { + return &NodeIterator{ + nodesToVisit: []*Node{}, + ndb: ndb, + }, nil + } + + node, err := ndb.GetNode(rootKey) + if err != nil { + return nil, err + } + + return &NodeIterator{ + nodesToVisit: []*Node{node}, + ndb: ndb, + }, nil +} + +// GetNode returns the current visiting node. +func (iter *NodeIterator) GetNode() *Node { + return iter.nodesToVisit[len(iter.nodesToVisit)-1] +} + +// Valid checks if the validator is valid. +func (iter *NodeIterator) Valid() bool { + return iter.err == nil && len(iter.nodesToVisit) > 0 +} + +// Error returns an error if any errors. +func (iter *NodeIterator) Error() error { + return iter.err +} + +// Next moves forward the traversal. +// if isSkipped is true, the subtree under the current node is skipped. +func (iter *NodeIterator) Next(isSkipped bool) { + if !iter.Valid() { + return + } + node := iter.GetNode() + iter.nodesToVisit = iter.nodesToVisit[:len(iter.nodesToVisit)-1] + + if isSkipped { + return + } + + if node.isLeaf() { + return + } + + rightNode, err := iter.ndb.GetNode(node.rightNodeKey) + if err != nil { + iter.err = err + return + } + iter.nodesToVisit = append(iter.nodesToVisit, rightNode) + + leftNode, err := iter.ndb.GetNode(node.leftNodeKey) + if err != nil { + iter.err = err + return + } + iter.nodesToVisit = append(iter.nodesToVisit, leftNode) +} diff --git a/iavl/iterator_test.go b/iavl/iterator_test.go new file mode 100644 index 000000000..d684bb8af --- /dev/null +++ b/iavl/iterator_test.go @@ -0,0 +1,383 @@ +package iavl + +import ( + "math/rand" + "sort" + "sync" + "testing" + + log "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +func TestIterator_NewIterator_NilTree_Failure(t *testing.T) { + start, end := []byte{'a'}, []byte{'c'} + ascending := true + + performTest := func(t *testing.T, itr dbm.Iterator) { + require.NotNil(t, itr) + require.False(t, itr.Valid()) + actualsStart, actualEnd := itr.Domain() + require.Equal(t, start, actualsStart) + require.Equal(t, end, actualEnd) + require.Error(t, itr.Error()) + } + + t.Run("Iterator", func(t *testing.T) { + itr := NewIterator(start, end, ascending, nil) + performTest(t, itr) + require.ErrorIs(t, errIteratorNilTreeGiven, itr.Error()) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr := NewFastIterator(start, end, ascending, nil) + performTest(t, itr) + require.ErrorIs(t, errFastIteratorNilNdbGiven, itr.Error()) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr := NewUnsavedFastIterator(start, end, ascending, nil, &sync.Map{}, &sync.Map{}) + performTest(t, itr) + require.ErrorIs(t, errFastIteratorNilNdbGiven, itr.Error()) + }) +} + +func TestUnsavedFastIterator_NewIterator_NilAdditions_Failure(t *testing.T) { + start, end := []byte{'a'}, []byte{'c'} + ascending := true + + performTest := func(t *testing.T, itr dbm.Iterator) { + require.NotNil(t, itr) + require.False(t, itr.Valid()) + actualsStart, actualEnd := itr.Domain() + require.Equal(t, start, actualsStart) + require.Equal(t, end, actualEnd) + require.Error(t, itr.Error()) + } + + t.Run("Nil additions given", func(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, nil, tree.unsavedFastNodeRemovals) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilAdditionsGiven, itr.Error()) + }) + + t.Run("Nil removals given", func(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, tree.unsavedFastNodeAdditions, nil) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilRemovalsGiven, itr.Error()) + }) + + t.Run("All nil", func(t *testing.T) { + itr := NewUnsavedFastIterator(start, end, ascending, nil, nil, nil) + performTest(t, itr) + require.ErrorIs(t, errFastIteratorNilNdbGiven, itr.Error()) + }) + + t.Run("Additions and removals are nil", func(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + itr := NewUnsavedFastIterator(start, end, ascending, tree.ndb, nil, nil) + performTest(t, itr) + require.ErrorIs(t, errUnsavedFastIteratorNilAdditionsGiven, itr.Error()) + }) +} + +func TestIterator_Empty_Invalid(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("a"), + endIterate: []byte("a"), + ascending: true, + } + + performTest := func(t *testing.T, itr dbm.Iterator, mirror [][]string) { + require.Equal(t, 0, len(mirror)) + require.False(t, itr.Valid()) + } + + t.Run("Iterator", func(t *testing.T) { + itr, mirror := setupIteratorAndMirror(t, config) + performTest(t, itr, mirror) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr, mirror := setupFastIteratorAndMirror(t, config) + performTest(t, itr, mirror) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr, mirror := setupUnsavedFastIterator(t, config) + performTest(t, itr, mirror) + }) +} + +func TestIterator_Basic_Ranged_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("e"), + endIterate: []byte("w"), + ascending: true, + } + iteratorSuccessTest(t, config) +} + +func TestIterator_Basic_Ranged_Descending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: []byte("e"), + endIterate: []byte("w"), + ascending: false, + } + iteratorSuccessTest(t, config) +} + +func TestIterator_Basic_Full_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: true, + } + + iteratorSuccessTest(t, config) +} + +func TestIterator_Basic_Full_Descending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: false, + } + iteratorSuccessTest(t, config) +} + +func TestIterator_WithDelete_Full_Ascending_Success(t *testing.T) { + config := &iteratorTestConfig{ + startByteToSet: 'a', + endByteToSet: 'z', + startIterate: nil, + endIterate: nil, + ascending: false, + } + + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + err = tree.DeleteVersionsTo(1) + require.NoError(t, err) + + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + immutableTree, err := tree.GetImmutable(latestVersion) + require.NoError(t, err) + + // sort mirror for assertion + sortedMirror := make([][]string, 0, len(mirror)) + for k, v := range mirror { + sortedMirror = append(sortedMirror, []string{k, v}) + } + + sort.Slice(sortedMirror, func(i, j int) bool { + return sortedMirror[i][0] > sortedMirror[j][0] + }) + + t.Run("Iterator", func(t *testing.T) { + itr := NewIterator(config.startIterate, config.endIterate, config.ascending, immutableTree) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr := NewFastIterator(config.startIterate, config.endIterate, config.ascending, immutableTree.ndb) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr := NewUnsavedFastIterator(config.startIterate, config.endIterate, config.ascending, immutableTree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals) + require.True(t, itr.Valid()) + assertIterator(t, itr, sortedMirror, config.ascending) + }) +} + +func iteratorSuccessTest(t *testing.T, config *iteratorTestConfig) { + performTest := func(t *testing.T, itr dbm.Iterator, mirror [][]string) { + actualStart, actualEnd := itr.Domain() + require.Equal(t, config.startIterate, actualStart) + require.Equal(t, config.endIterate, actualEnd) + + require.NoError(t, itr.Error()) + + assertIterator(t, itr, mirror, config.ascending) + } + + t.Run("Iterator", func(t *testing.T) { + itr, mirror := setupIteratorAndMirror(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) + + t.Run("Fast Iterator", func(t *testing.T) { + itr, mirror := setupFastIteratorAndMirror(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) + + t.Run("Unsaved Fast Iterator", func(t *testing.T) { + itr, mirror := setupUnsavedFastIterator(t, config) + require.True(t, itr.Valid()) + performTest(t, itr, mirror) + }) +} + +func setupIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (dbm.Iterator, [][]string) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + mirror := setupMirrorForIterator(t, config, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + immutableTree, err := tree.GetImmutable(latestVersion) + require.NoError(t, err) + + itr := NewIterator(config.startIterate, config.endIterate, config.ascending, immutableTree) + return itr, mirror +} + +func setupFastIteratorAndMirror(t *testing.T, config *iteratorTestConfig) (dbm.Iterator, [][]string) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + mirror := setupMirrorForIterator(t, config, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + itr := NewFastIterator(config.startIterate, config.endIterate, config.ascending, tree.ndb) + return itr, mirror +} + +func setupUnsavedFastIterator(t *testing.T, config *iteratorTestConfig) (dbm.Iterator, [][]string) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + // For unsaved fast iterator, we would like to test the state where + // there are saved fast nodes as well as some unsaved additions and removals. + // So, we split the byte range in half where the first half is saved and the second half is unsaved. + breakpointByte := (config.endByteToSet + config.startByteToSet) / 2 + + firstHalfConfig := *config + firstHalfConfig.endByteToSet = breakpointByte // exclusive + + secondHalfConfig := *config + secondHalfConfig.startByteToSet = breakpointByte + + // First half of the mirror + mirror := setupMirrorForIterator(t, &firstHalfConfig, tree) + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + // No unsaved additions or removals should be present after saving + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeAdditions)) + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeRemovals)) + + // Ensure that there are unsaved additions and removals present + secondHalfMirror := setupMirrorForIterator(t, &secondHalfConfig, tree) + + require.True(t, syncMapCount(tree.unsavedFastNodeAdditions) >= len(secondHalfMirror)) + require.Equal(t, 0, syncMapCount(tree.unsavedFastNodeRemovals)) + + // Merge the two halves + if config.ascending { + mirror = append(mirror, secondHalfMirror...) + } else { + mirror = append(secondHalfMirror, mirror...) + } + + if len(mirror) > 0 { + // Remove random keys + for i := 0; i < len(mirror)/4; i++ { + randIndex := rand.Intn(len(mirror)) + keyToRemove := mirror[randIndex][0] + + _, removed, err := tree.Remove([]byte(keyToRemove)) + require.NoError(t, err) + require.True(t, removed) + + mirror = append(mirror[:randIndex], mirror[randIndex+1:]...) + } + } + + itr := NewUnsavedFastIterator(config.startIterate, config.endIterate, config.ascending, tree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals) + return itr, mirror +} + +func TestNodeIterator_Success(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + // check if the iterating count is same with the entire node count of the tree + itr, err := NewNodeIterator(tree.root.GetKey(), tree.ndb) + require.NoError(t, err) + nodeCount := 0 + for ; itr.Valid(); itr.Next(false) { + nodeCount++ + } + require.Equal(t, int64(nodeCount), tree.Size()*2-1) + + // check if the skipped node count is right + itr, err = NewNodeIterator(tree.root.GetKey(), tree.ndb) + require.NoError(t, err) + updateCount := 0 + skipCount := 0 + for itr.Valid() { + node := itr.GetNode() + updateCount++ + if node.nodeKey.version < tree.Version() { + skipCount += int(node.size*2 - 2) // the size of the subtree without the root + } + itr.Next(node.nodeKey.version < tree.Version()) + } + require.Equal(t, nodeCount, updateCount+skipCount) +} + +func TestNodeIterator_WithEmptyRoot(t *testing.T) { + itr, err := NewNodeIterator(nil, newNodeDB(dbm.NewMemDB(), 0, DefaultOptions(), log.NewNopLogger())) + require.NoError(t, err) + require.False(t, itr.Valid()) + + itr, err = NewNodeIterator([]byte{}, newNodeDB(dbm.NewMemDB(), 0, DefaultOptions(), log.NewNopLogger())) + require.NoError(t, err) + require.False(t, itr.Valid()) +} + +func syncMapCount(m *sync.Map) int { + count := 0 + m.Range(func(_, _ interface{}) bool { + count++ + return true + }) + return count +} diff --git a/iavl/keyformat/key_format.go b/iavl/keyformat/key_format.go new file mode 100644 index 000000000..1614ab0a5 --- /dev/null +++ b/iavl/keyformat/key_format.go @@ -0,0 +1,206 @@ +package keyformat + +import ( + "encoding/binary" + "fmt" + "math/big" +) + +// Provides a fixed-width lexicographically sortable []byte key format +type KeyFormat struct { + layout []int + length int + prefix byte + unbounded bool +} + +// Create a []byte key format based on a single byte prefix and fixed width key segments each of whose length is +// specified by by the corresponding element of layout. +// +// For example, to store keys that could index some objects by a version number and their SHA256 hash using the form: +// 'c' then you would define the KeyFormat with: +// +// var keyFormat = NewKeyFormat('c', 8, 32) +// +// Then you can create a key with: +// +// func ObjectKey(version uint64, objectBytes []byte) []byte { +// hasher := sha256.New() +// hasher.Sum(nil) +// return keyFormat.Key(version, hasher.Sum(nil)) +// } +// +// If the last term of the layout is 0 it is unbounded, and will accept any length. +// +// NewKeyFormat panics if any other term than the last is 0. +func NewKeyFormat(prefix byte, layout ...int) *KeyFormat { + // For prefix byte + length := 1 + for i, l := range layout { + length += l + if l == 0 && i != len(layout)-1 { + panic("Only the last item in a key format can be 0") + } + } + return &KeyFormat{ + prefix: prefix, + layout: layout, + length: length, + unbounded: len(layout) > 0 && layout[len(layout)-1] == 0, + } +} + +// Format the byte segments into the key format - will panic if the segment lengths do not match the layout. +func (kf *KeyFormat) KeyBytes(segments ...[]byte) []byte { + keyLen := kf.length + // In case segments length is less than layouts length, + // we don't have to allocate the whole kf.length, just + // enough space to store the segments. + if len(segments) < len(kf.layout) { + keyLen = 1 + for i := range segments { + keyLen += kf.layout[i] + } + } + + if kf.unbounded { + if len(segments) > 0 { + keyLen += len(segments[len(segments)-1]) + } + } + key := make([]byte, keyLen) + key[0] = kf.prefix + n := 1 + for i, s := range segments { + l := kf.layout[i] + + switch l { + case 0: + // If the expected segment length is unbounded, increase it by `string length` + n += len(s) + default: + if len(s) > l { + panic(fmt.Errorf("length of segment %X provided to KeyFormat.KeyBytes() is longer than the %d bytes "+ + "required by layout for segment %d", s, l, i)) + } + // Otherwise increase n by the segment length + n += l + + } + // Big endian so pad on left if not given the full width for this segment + copy(key[n-len(s):n], s) + } + return key[:n] +} + +// Format the args passed into the key format - will panic if the arguments passed do not match the length +// of the segment to which they correspond. When called with no arguments returns the raw prefix (useful as a start +// element of the entire keys space when sorted lexicographically). +func (kf *KeyFormat) Key(args ...interface{}) []byte { + if len(args) > len(kf.layout) { + panic(fmt.Errorf("keyFormat.Key() is provided with %d args but format only has %d segments", + len(args), len(kf.layout))) + } + segments := make([][]byte, len(args)) + for i, a := range args { + segments[i] = format(a) + } + return kf.KeyBytes(segments...) +} + +// Reads out the bytes associated with each segment of the key format from key. +func (kf *KeyFormat) ScanBytes(key []byte) [][]byte { + segments := make([][]byte, len(kf.layout)) + n := 1 + for i, l := range kf.layout { + n += l + // if current section is longer than key, then there are no more subsequent segments. + if n > len(key) { + return segments[:i] + } + // if unbounded, segment is rest of key + if l == 0 { + segments[i] = key[n:] + break + } + segments[i] = key[n-l : n] + } + return segments +} + +// Extracts the segments into the values pointed to by each of args. Each arg must be a pointer to int64, uint64, or +// []byte, and the width of the args must match layout. +func (kf *KeyFormat) Scan(key []byte, args ...interface{}) { + segments := kf.ScanBytes(key) + if len(args) > len(segments) { + panic(fmt.Errorf("keyFormat.Scan() is provided with %d args but format only has %d segments in key %X", + len(args), len(segments), key)) + } + for i, a := range args { + scan(a, segments[i]) + } +} + +// Length of the key format. +func (kf *KeyFormat) Length() int { + return kf.length +} + +// Return the prefix as a string. +func (kf *KeyFormat) Prefix() string { + return string([]byte{kf.prefix}) +} + +func scan(a interface{}, value []byte) { + switch v := a.(type) { + case *int64: + // Negative values will be mapped correctly when read in as uint64 and then type converted + *v = int64(binary.BigEndian.Uint64(value)) + case *uint64: + *v = binary.BigEndian.Uint64(value) + case *uint32: + *v = binary.BigEndian.Uint32(value) + case *int32: + *v = int32(binary.BigEndian.Uint32(value)) + case *[]byte: + *v = value + case *big.Int: + *v = *big.NewInt(0).SetBytes(value) + default: + panic(fmt.Errorf("keyFormat scan() does not support scanning value of type %T: %v", a, a)) + } +} + +func format(a interface{}) []byte { + switch v := a.(type) { + case uint64: + return formatUint64(v) + case int64: + return formatUint64(uint64(v)) + // Provide formatting from int,uint as a convenience to avoid casting arguments + case uint: + return formatUint64(uint64(v)) + case int: + return formatUint64(uint64(v)) + case uint32: + return formatUint32(v) + case int32: + return formatUint32(uint32(v)) + case []byte: + return v + default: + panic(fmt.Errorf("keyFormat format() does not support formatting value of type %T: %v", a, a)) + } +} + +func formatUint64(v uint64) []byte { + bs := make([]byte, 8) + binary.BigEndian.PutUint64(bs, v) + return bs +} + +func formatUint32(v uint32) []byte { + bs := make([]byte, 4) + binary.BigEndian.PutUint32(bs, v) + return bs +} diff --git a/iavl/keyformat/key_format_test.go b/iavl/keyformat/key_format_test.go new file mode 100644 index 000000000..127c18dfb --- /dev/null +++ b/iavl/keyformat/key_format_test.go @@ -0,0 +1,152 @@ +package keyformat + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeyFormatBytes(t *testing.T) { + type keyPairs struct { + key [][]byte + expected []byte + } + emptyTestVector := keyPairs{key: [][]byte{}, expected: []byte{'e'}} + threeByteTestVector := keyPairs{ + key: [][]byte{{1, 2, 3}}, + expected: []byte{'e', 0, 0, 0, 0, 0, 1, 2, 3}, + } + eightByteTestVector := keyPairs{ + key: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8}}, + expected: []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8}, + } + + tests := []struct { + name string + kf *KeyFormat + testVectors []keyPairs + }{{ + name: "simple 3 int key format", + kf: NewKeyFormat(byte('e'), 8, 8, 8), + testVectors: []keyPairs{ + emptyTestVector, + threeByteTestVector, + eightByteTestVector, + { + key: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, {1, 1, 2, 2, 3, 3}}, + expected: []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 1, 1, 2, 2, 3, 3}, + }, + }, + }, { + name: "zero suffix key format", + kf: NewKeyFormat(byte('e'), 8, 0), + testVectors: []keyPairs{ + emptyTestVector, + threeByteTestVector, + eightByteTestVector, + { + key: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8, 9}}, + expected: []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + }, + { + key: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8}, []byte("hellohello")}, + expected: []byte{'e', 1, 2, 3, 4, 5, 6, 7, 8, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x68, 0x65, 0x6c, 0x6c, 0x6f}, + }, + }, + }} + for _, tc := range tests { + kf := tc.kf + for i, v := range tc.testVectors { + assert.Equal(t, v.expected, kf.KeyBytes(v.key...), "key format %s, test case %d", tc.name, i) + } + } +} + +func TestKeyFormat(t *testing.T) { + kf := NewKeyFormat(byte('e'), 8, 8, 8) + key := []byte{'e', 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 200, 0, 0, 0, 0, 0, 0, 1, 144} + var a, b, c int64 = 100, 200, 400 + assert.Equal(t, key, kf.Key(a, b, c)) + + ao, bo, co := new(int64), new(int64), new(int64) + kf.Scan(key, ao, bo, co) + assert.Equal(t, a, *ao) + assert.Equal(t, b, *bo) + assert.Equal(t, c, *co) + + bs := new([]byte) + kf.Scan(key, ao, bo, bs) + assert.Equal(t, a, *ao) + assert.Equal(t, b, *bo) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 1, 144}, *bs) + + assert.Equal(t, []byte{'e', 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 200}, kf.Key(a, b)) +} + +func TestNegativeKeys(t *testing.T) { + kf := NewKeyFormat(byte('e'), 8, 8) + + var a, b int64 = -100, -200 + // One's complement plus one + key := []byte{ + 'e', + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, byte(0xff + a + 1), + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, byte(0xff + b + 1), + } + assert.Equal(t, key, kf.Key(a, b)) + + ao, bo := new(int64), new(int64) + kf.Scan(key, ao, bo) + assert.Equal(t, a, *ao) + assert.Equal(t, b, *bo) +} + +func TestOverflow(t *testing.T) { + kf := NewKeyFormat(byte('o'), 8, 8) + + var a int64 = 1 << 62 + var b uint64 = 1 << 63 + key := []byte{ + 'o', + 0x40, 0, 0, 0, 0, 0, 0, 0, + 0x80, 0, 0, 0, 0, 0, 0, 0, + } + assert.Equal(t, key, kf.Key(a, b)) + + ao, bo := new(int64), new(int64) + kf.Scan(key, ao, bo) + assert.Equal(t, a, *ao) + assert.Equal(t, int64(b), *bo) +} + +func benchmarkKeyFormatBytes(b *testing.B, kf *KeyFormat, segments ...[]byte) { + for i := 0; i < b.N; i++ { + kf.KeyBytes(segments...) + } +} + +func BenchmarkKeyFormat_KeyBytesOneSegment(b *testing.B) { + benchmarkKeyFormatBytes(b, NewKeyFormat('e', 8, 8, 8), nil) +} + +func BenchmarkKeyFormat_KeyBytesThreeSegment(b *testing.B) { + segments := [][]byte{ + {1, 2, 3, 4, 5, 6, 7, 8}, + {1, 2, 3, 4, 5, 6, 7, 8}, + {1, 1, 2, 2, 3, 3}, + } + benchmarkKeyFormatBytes(b, NewKeyFormat('e', 8, 8, 8), segments...) +} + +func BenchmarkKeyFormat_KeyBytesOneSegmentWithVariousLayouts(b *testing.B) { + benchmarkKeyFormatBytes(b, NewKeyFormat('e', 8, 16, 32), nil) +} + +func BenchmarkKeyFormat_KeyBytesThreeSegmentWithVariousLayouts(b *testing.B) { + segments := [][]byte{ + {1, 2, 3, 4, 5, 6, 7, 8}, + {1, 2, 3, 4, 5, 6, 7, 8}, + {1, 1, 2, 2, 3, 3}, + } + benchmarkKeyFormatBytes(b, NewKeyFormat('e', 8, 16, 32), segments...) +} diff --git a/iavl/keyformat/prefix_formatter.go b/iavl/keyformat/prefix_formatter.go new file mode 100644 index 000000000..873c3a7e1 --- /dev/null +++ b/iavl/keyformat/prefix_formatter.go @@ -0,0 +1,42 @@ +package keyformat + +import "encoding/binary" + +// This file builds some dedicated key formatters for what appears in benchmarks. + +// Prefixes a single byte before a 32 byte hash. +type FastPrefixFormatter struct { + prefix byte + length int + prefixSlice []byte +} + +func NewFastPrefixFormatter(prefix byte, length int) *FastPrefixFormatter { + return &FastPrefixFormatter{prefix: prefix, length: length, prefixSlice: []byte{prefix}} +} + +func (f *FastPrefixFormatter) Key(bz []byte) []byte { + key := make([]byte, 1+f.length) + key[0] = f.prefix + copy(key[1:], bz) + return key +} + +func (f *FastPrefixFormatter) Scan(key []byte, a interface{}) { + scan(a, key[1:]) +} + +func (f *FastPrefixFormatter) KeyInt64(bz int64) []byte { + key := make([]byte, 1+f.length) + key[0] = f.prefix + binary.BigEndian.PutUint64(key[1:], uint64(bz)) + return key +} + +func (f *FastPrefixFormatter) Prefix() []byte { + return f.prefixSlice +} + +func (f *FastPrefixFormatter) Length() int { + return 1 + f.length +} diff --git a/iavl/migrate_test.go b/iavl/migrate_test.go new file mode 100644 index 000000000..adc238dd1 --- /dev/null +++ b/iavl/migrate_test.go @@ -0,0 +1,345 @@ +package iavl + +import ( + "bytes" + "fmt" + "math/rand" + "os" + "os/exec" + "path" + "testing" + "time" + + "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +const ( + dbType = "goleveldb" +) + +func createLegacyTree(t *testing.T, dbDir string, version int) (string, error) { + relateDir := path.Join(t.TempDir(), dbDir) + if _, err := os.Stat(relateDir); err == nil { + err := os.RemoveAll(relateDir) + if err != nil { + t.Errorf("%+v\n", err) + } + } + + cmd := exec.Command("sh", "-c", fmt.Sprintf("./cmd/legacydump/legacydump %s %s random %d %d", dbType, relateDir, version, version/2)) //nolint:gosec + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil || stderr.Len() > 0 { + t.Log(fmt.Sprint(err) + ": " + stderr.String()) + if err == nil { + err = fmt.Errorf("stderr: %s", stderr.String()) + } + } + t.Log("Result: " + out.String()) + + return relateDir, err +} + +func TestLazySet(t *testing.T) { + legacyVersion := 1000 + dbDir := fmt.Sprintf("legacy-%s-%d", dbType, legacyVersion) + relateDir, err := createLegacyTree(t, dbDir, legacyVersion) + require.NoError(t, err) + + db, err := dbm.NewDB("test", dbType, relateDir) + require.NoError(t, err) + + defer func() { + if err := db.Close(); err != nil { + t.Errorf("DB close error: %v\n", err) + } + if err := os.RemoveAll(relateDir); err != nil { + t.Errorf("%+v\n", err) + } + }() + + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Load the latest legacy version + _, err = tree.LoadVersion(int64(legacyVersion)) + require.NoError(t, err) + + // Commit new versions + postVersions := 1000 + for i := 0; i < postVersions; i++ { + leafCount := rand.Intn(50) + for j := 0; j < leafCount; j++ { + _, err = tree.Set([]byte(fmt.Sprintf("key-%d-%d", i, j)), []byte(fmt.Sprintf("value-%d-%d", i, j))) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + tree = NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Verify that the latest legacy version can still be loaded + _, err = tree.LoadVersion(int64(legacyVersion)) + require.NoError(t, err) +} + +func TestLegacyReferenceNode(t *testing.T) { + legacyVersion := 20 + dbDir := fmt.Sprintf("./legacy-%s-%d", dbType, legacyVersion) + relateDir, err := createLegacyTree(t, dbDir, legacyVersion) + require.NoError(t, err) + + db, err := dbm.NewDB("test", dbType, relateDir) + require.NoError(t, err) + + defer func() { + if err := db.Close(); err != nil { + t.Errorf("DB close error: %v\n", err) + } + if err := os.RemoveAll(relateDir); err != nil { + t.Errorf("%+v\n", err) + } + }() + + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Load the latest legacy version + _, err = tree.LoadVersion(int64(legacyVersion)) + require.NoError(t, err) + legacyLatestVersion := tree.root.nodeKey.version + + // Commit new versions without updates + _, _, err = tree.SaveVersion() + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + + // Load the previous version + newTree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + _, err = newTree.LoadVersion(version - 1) + require.NoError(t, err) + // Check if the reference node is refactored + require.Equal(t, newTree.root.nodeKey.nonce, uint32(0)) + require.Equal(t, newTree.root.nodeKey.version, legacyLatestVersion) +} + +func TestDeleteVersions(t *testing.T) { + legacyVersion := 100 + dbDir := fmt.Sprintf("./legacy-%s-%d", dbType, legacyVersion) + relateDir, err := createLegacyTree(t, dbDir, legacyVersion) + require.NoError(t, err) + + db, err := dbm.NewDB("test", dbType, relateDir) + require.NoError(t, err) + + defer func() { + if err := db.Close(); err != nil { + t.Errorf("DB close error: %v\n", err) + } + if err := os.RemoveAll(relateDir); err != nil { + t.Errorf("%+v\n", err) + } + }() + + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Load the latest legacy version + _, err = tree.LoadVersion(int64(legacyVersion)) + require.NoError(t, err) + + // Commit new versions + postVersions := 100 + for i := 0; i < postVersions; i++ { + leafCount := rand.Intn(10) + for j := 0; j < leafCount; j++ { + _, err = tree.Set([]byte(fmt.Sprintf("key-%d-%d", i, j)), []byte(fmt.Sprintf("value-%d-%d", i, j))) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + // Check the available versions + versions := tree.AvailableVersions() + targetVersion := 0 + for i := len(versions) - 1; i >= 0; i-- { + if versions[i] < legacyVersion { + targetVersion = versions[i] + break + } + } + + // Test LoadVersionForOverwriting for the legacy version + err = tree.LoadVersionForOverwriting(int64(targetVersion)) + require.NoError(t, err) + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(targetVersion), latestVersion) + legacyLatestVersion, err := tree.ndb.getLegacyLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(targetVersion), legacyLatestVersion) + + // Test DeleteVersionsTo for the legacy version + for i := 0; i < postVersions; i++ { + leafCount := rand.Intn(20) + for j := 0; j < leafCount; j++ { + _, err = tree.Set([]byte(fmt.Sprintf("key-%d-%d", i, j)), []byte(fmt.Sprintf("value-%d-%d", i, j))) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + // Check if the legacy versions are deleted at once + versions = tree.AvailableVersions() + err = tree.DeleteVersionsTo(legacyLatestVersion - 1) + require.NoError(t, err) + pVersions := tree.AvailableVersions() + require.Equal(t, len(versions), len(pVersions)) + toVersion := legacyLatestVersion + int64(postVersions)/2 + err = tree.DeleteVersionsTo(toVersion) + require.NoError(t, err) + pVersions = tree.AvailableVersions() + require.Equal(t, postVersions/2, len(pVersions)) +} + +func TestPruning(t *testing.T) { + legacyVersion := 100 + dbDir := fmt.Sprintf("./legacy-%s-%d", dbType, legacyVersion) + relateDir, err := createLegacyTree(t, dbDir, legacyVersion) + require.NoError(t, err) + + db, err := dbm.NewDB("test", dbType, relateDir) + require.NoError(t, err) + + defer func() { + if err := db.Close(); err != nil { + t.Errorf("DB close error: %v\n", err) + } + if err := os.RemoveAll(relateDir); err != nil { + t.Errorf("%+v\n", err) + } + }() + + // Load the latest version + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + + // Save 10 versions without updates + for i := 0; i < 10; i++ { + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + // Save 990 versions + leavesCount := 10 + toVersion := int64(990) + pruningInterval := int64(20) + for i := int64(0); i < toVersion; i++ { + for j := 0; j < leavesCount; j++ { + _, err := tree.Set([]byte(fmt.Sprintf("key%d", j)), []byte(fmt.Sprintf("value%d", j))) + require.NoError(t, err) + } + _, v, err := tree.SaveVersion() + require.NoError(t, err) + if v%pruningInterval == 0 { + err = tree.DeleteVersionsTo(v - pruningInterval/2) + require.NoError(t, err) + } + } + + // Wait for pruning to finish + for i := 0; i < 100; i++ { + _, _, err := tree.SaveVersion() + require.NoError(t, err) + isLeacy, err := tree.ndb.hasLegacyVersion(int64(legacyVersion)) + require.NoError(t, err) + if !isLeacy { + break + } + // Simulate the consensus state update + time.Sleep(500 * time.Millisecond) + } + // Reload the tree + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + versions := tree.AvailableVersions() + require.Equal(t, versions[0], int(toVersion)+legacyVersion+1) + for _, v := range versions { + _, err := tree.LoadVersion(int64(v)) + require.NoError(t, err) + } + // Check if the legacy nodes are pruned + _, err = tree.Load() + require.NoError(t, err) + itr, err := NewNodeIterator(tree.root.GetKey(), tree.ndb) + require.NoError(t, err) + legacyNodes := make(map[string]*Node) + for ; itr.Valid(); itr.Next(false) { + node := itr.GetNode() + if node.nodeKey.nonce == 0 { + legacyNodes[string(node.hash)] = node + } + } + + lNodes, err := tree.ndb.legacyNodes() + require.NoError(t, err) + require.Len(t, lNodes, len(legacyNodes)) + for _, node := range lNodes { + _, ok := legacyNodes[string(node.hash)] + require.True(t, ok) + } +} + +func TestRandomSet(t *testing.T) { + legacyVersion := 50 + dbDir := fmt.Sprintf("./legacy-%s-%d", dbType, legacyVersion) + relateDir, err := createLegacyTree(t, dbDir, legacyVersion) + require.NoError(t, err) + + db, err := dbm.NewDB("test", dbType, relateDir) + require.NoError(t, err) + + defer func() { + if err := db.Close(); err != nil { + t.Errorf("DB close error: %v\n", err) + } + if err := os.RemoveAll(relateDir); err != nil { + t.Errorf("%+v\n", err) + } + }() + + tree := NewMutableTree(db, 10000, false, log.NewNopLogger()) + + // Load the latest legacy version + _, err = tree.LoadVersion(int64(legacyVersion)) + require.NoError(t, err) + + // Commit new versions + postVersions := 1000 + emptyVersions := 10 + for i := 0; i < emptyVersions; i++ { + _, _, err := tree.SaveVersion() + require.NoError(t, err) + } + for i := 0; i < postVersions-emptyVersions; i++ { + leafCount := rand.Intn(50) + for j := 0; j < leafCount; j++ { + key := iavlrand.RandBytes(10) + value := iavlrand.RandBytes(10) + _, err = tree.Set(key, value) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + err = tree.DeleteVersionsTo(int64(legacyVersion + postVersions - 1)) + require.NoError(t, err) +} diff --git a/iavl/mock/db_mock.go b/iavl/mock/db_mock.go new file mode 100644 index 000000000..4c4d3d515 --- /dev/null +++ b/iavl/mock/db_mock.go @@ -0,0 +1,365 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./db/types.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + db "github.com/cosmos/iavl/db" + gomock "github.com/golang/mock/gomock" +) + +// MockDB is a mock of DB interface. +type MockDB struct { + ctrl *gomock.Controller + recorder *MockDBMockRecorder +} + +// MockDBMockRecorder is the mock recorder for MockDB. +type MockDBMockRecorder struct { + mock *MockDB +} + +// NewMockDB creates a new mock instance. +func NewMockDB(ctrl *gomock.Controller) *MockDB { + mock := &MockDB{ctrl: ctrl} + mock.recorder = &MockDBMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDB) EXPECT() *MockDBMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockDB) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockDBMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDB)(nil).Close)) +} + +// Get mocks base method. +func (m *MockDB) Get(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDBMockRecorder) Get(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), arg0) +} + +// Has mocks base method. +func (m *MockDB) Has(key []byte) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Has", key) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Has indicates an expected call of Has. +func (mr *MockDBMockRecorder) Has(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockDB)(nil).Has), key) +} + +// Iterator mocks base method. +func (m *MockDB) Iterator(start, end []byte) (db.Iterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Iterator", start, end) + ret0, _ := ret[0].(db.Iterator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Iterator indicates an expected call of Iterator. +func (mr *MockDBMockRecorder) Iterator(start, end interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Iterator", reflect.TypeOf((*MockDB)(nil).Iterator), start, end) +} + +// NewBatch mocks base method. +func (m *MockDB) NewBatch() db.Batch { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewBatch") + ret0, _ := ret[0].(db.Batch) + return ret0 +} + +// NewBatch indicates an expected call of NewBatch. +func (mr *MockDBMockRecorder) NewBatch() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockDB)(nil).NewBatch)) +} + +// NewBatchWithSize mocks base method. +func (m *MockDB) NewBatchWithSize(arg0 int) db.Batch { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewBatchWithSize", arg0) + ret0, _ := ret[0].(db.Batch) + return ret0 +} + +// NewBatchWithSize indicates an expected call of NewBatchWithSize. +func (mr *MockDBMockRecorder) NewBatchWithSize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithSize", reflect.TypeOf((*MockDB)(nil).NewBatchWithSize), arg0) +} + +// ReverseIterator mocks base method. +func (m *MockDB) ReverseIterator(start, end []byte) (db.Iterator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReverseIterator", start, end) + ret0, _ := ret[0].(db.Iterator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReverseIterator indicates an expected call of ReverseIterator. +func (mr *MockDBMockRecorder) ReverseIterator(start, end interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReverseIterator", reflect.TypeOf((*MockDB)(nil).ReverseIterator), start, end) +} + +// MockIterator is a mock of Iterator interface. +type MockIterator struct { + ctrl *gomock.Controller + recorder *MockIteratorMockRecorder +} + +// MockIteratorMockRecorder is the mock recorder for MockIterator. +type MockIteratorMockRecorder struct { + mock *MockIterator +} + +// NewMockIterator creates a new mock instance. +func NewMockIterator(ctrl *gomock.Controller) *MockIterator { + mock := &MockIterator{ctrl: ctrl} + mock.recorder = &MockIteratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIterator) EXPECT() *MockIteratorMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockIterator) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockIteratorMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIterator)(nil).Close)) +} + +// Domain mocks base method. +func (m *MockIterator) Domain() ([]byte, []byte) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Domain") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].([]byte) + return ret0, ret1 +} + +// Domain indicates an expected call of Domain. +func (mr *MockIteratorMockRecorder) Domain() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Domain", reflect.TypeOf((*MockIterator)(nil).Domain)) +} + +// Error mocks base method. +func (m *MockIterator) Error() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Error") + ret0, _ := ret[0].(error) + return ret0 +} + +// Error indicates an expected call of Error. +func (mr *MockIteratorMockRecorder) Error() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockIterator)(nil).Error)) +} + +// Key mocks base method. +func (m *MockIterator) Key() []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Key") + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Key indicates an expected call of Key. +func (mr *MockIteratorMockRecorder) Key() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockIterator)(nil).Key)) +} + +// Next mocks base method. +func (m *MockIterator) Next() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Next") +} + +// Next indicates an expected call of Next. +func (mr *MockIteratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockIterator)(nil).Next)) +} + +// Valid mocks base method. +func (m *MockIterator) Valid() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Valid") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Valid indicates an expected call of Valid. +func (mr *MockIteratorMockRecorder) Valid() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Valid", reflect.TypeOf((*MockIterator)(nil).Valid)) +} + +// Value mocks base method. +func (m *MockIterator) Value() []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Value") + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Value indicates an expected call of Value. +func (mr *MockIteratorMockRecorder) Value() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockIterator)(nil).Value)) +} + +// MockBatch is a mock of Batch interface. +type MockBatch struct { + ctrl *gomock.Controller + recorder *MockBatchMockRecorder +} + +// MockBatchMockRecorder is the mock recorder for MockBatch. +type MockBatchMockRecorder struct { + mock *MockBatch +} + +// NewMockBatch creates a new mock instance. +func NewMockBatch(ctrl *gomock.Controller) *MockBatch { + mock := &MockBatch{ctrl: ctrl} + mock.recorder = &MockBatchMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBatch) EXPECT() *MockBatchMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockBatch) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockBatchMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockBatch)(nil).Close)) +} + +// Delete mocks base method. +func (m *MockBatch) Delete(key []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", key) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockBatchMockRecorder) Delete(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBatch)(nil).Delete), key) +} + +// GetByteSize mocks base method. +func (m *MockBatch) GetByteSize() (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByteSize") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByteSize indicates an expected call of GetByteSize. +func (mr *MockBatchMockRecorder) GetByteSize() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByteSize", reflect.TypeOf((*MockBatch)(nil).GetByteSize)) +} + +// Set mocks base method. +func (m *MockBatch) Set(key, value []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Set", key, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// Set indicates an expected call of Set. +func (mr *MockBatchMockRecorder) Set(key, value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockBatch)(nil).Set), key, value) +} + +// Write mocks base method. +func (m *MockBatch) Write() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write") + ret0, _ := ret[0].(error) + return ret0 +} + +// Write indicates an expected call of Write. +func (mr *MockBatchMockRecorder) Write() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockBatch)(nil).Write)) +} + +// WriteSync mocks base method. +func (m *MockBatch) WriteSync() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteSync") + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteSync indicates an expected call of WriteSync. +func (mr *MockBatchMockRecorder) WriteSync() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteSync", reflect.TypeOf((*MockBatch)(nil).WriteSync)) +} diff --git a/iavl/mockgen.sh b/iavl/mockgen.sh new file mode 100755 index 000000000..5300f9ce5 --- /dev/null +++ b/iavl/mockgen.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +mockgen_cmd="mockgen" +$mockgen_cmd -package mock -destination mock/db_mock.go github.com/cosmos/cosmos-db DB,Iterator,Batch \ No newline at end of file diff --git a/iavl/mutable_tree.go b/iavl/mutable_tree.go new file mode 100644 index 000000000..3ec57eb87 --- /dev/null +++ b/iavl/mutable_tree.go @@ -0,0 +1,1088 @@ +package iavl + +import ( + "bytes" + "errors" + "fmt" + "sort" + "sync" + + log "cosmossdk.io/log" + + dbm "github.com/cosmos/iavl/db" + "github.com/cosmos/iavl/fastnode" + ibytes "github.com/cosmos/iavl/internal/bytes" +) + +var ( + // ErrVersionDoesNotExist is returned if a requested version does not exist. + ErrVersionDoesNotExist = errors.New("version does not exist") + + // ErrKeyDoesNotExist is returned if a key does not exist. + ErrKeyDoesNotExist = errors.New("key does not exist") +) + +type Option func(*Options) + +// MutableTree is a persistent tree which keeps track of versions. It is not safe for concurrent +// use, and should be guarded by a Mutex or RWLock as appropriate. An immutable tree at a given +// version can be returned via GetImmutable, which is safe for concurrent access. +// +// Given and returned key/value byte slices must not be modified, since they may point to data +// located inside IAVL which would also be modified. +// +// The inner ImmutableTree should not be used directly by callers. +type MutableTree struct { + logger log.Logger + + *ImmutableTree // The current, working tree. + lastSaved *ImmutableTree // The most recently saved tree. + unsavedFastNodeAdditions *sync.Map // map[string]*FastNode FastNodes that have not yet been saved to disk + unsavedFastNodeRemovals *sync.Map // map[string]interface{} FastNodes that have not yet been removed from disk + ndb *nodeDB + skipFastStorageUpgrade bool // If true, the tree will work like no fast storage and always not upgrade fast storage + + mtx sync.Mutex +} + +// NewMutableTree returns a new tree with the specified optional options. +func NewMutableTree(db dbm.DB, cacheSize int, skipFastStorageUpgrade bool, lg log.Logger, options ...Option) *MutableTree { + opts := DefaultOptions() + for _, opt := range options { + opt(&opts) + } + + ndb := newNodeDB(db, cacheSize, opts, lg) + head := &ImmutableTree{ndb: ndb, skipFastStorageUpgrade: skipFastStorageUpgrade} + + return &MutableTree{ + logger: lg, + ImmutableTree: head, + lastSaved: head.clone(), + unsavedFastNodeAdditions: &sync.Map{}, + unsavedFastNodeRemovals: &sync.Map{}, + ndb: ndb, + skipFastStorageUpgrade: skipFastStorageUpgrade, + } +} + +// IsEmpty returns whether or not the tree has any keys. Only trees that are +// not empty can be saved. +func (tree *MutableTree) IsEmpty() bool { + return tree.ImmutableTree.Size() == 0 +} + +// VersionExists returns whether or not a version exists. +func (tree *MutableTree) VersionExists(version int64) bool { + legacyLatestVersion, err := tree.ndb.getLegacyLatestVersion() + if err != nil { + return false + } + if version <= legacyLatestVersion { + has, err := tree.ndb.hasLegacyVersion(version) + return err == nil && has + } + firstVersion, err := tree.ndb.getFirstVersion() + if err != nil { + return false + } + latestVersion, err := tree.ndb.getLatestVersion() + if err != nil { + return false + } + + return firstVersion <= version && version <= latestVersion +} + +// AvailableVersions returns all available versions in ascending order +func (tree *MutableTree) AvailableVersions() []int { + firstVersion, err := tree.ndb.getFirstVersion() + if err != nil { + return nil + } + latestVersion, err := tree.ndb.getLatestVersion() + if err != nil { + return nil + } + legacyLatestVersion, err := tree.ndb.getLegacyLatestVersion() + if err != nil { + return nil + } + + res := make([]int, 0) + if legacyLatestVersion > firstVersion { + for version := firstVersion; version < legacyLatestVersion; version++ { + has, err := tree.ndb.hasLegacyVersion(version) + if err != nil { + return nil + } + if has { + res = append(res, int(version)) + } + } + firstVersion = legacyLatestVersion + } + + for version := firstVersion; version <= latestVersion; version++ { + res = append(res, int(version)) + } + return res +} + +// Hash returns the hash of the latest saved version of the tree, as returned +// by SaveVersion. If no versions have been saved, Hash returns nil. +func (tree *MutableTree) Hash() []byte { + return tree.lastSaved.Hash() +} + +// WorkingHash returns the hash of the current working tree. +func (tree *MutableTree) WorkingHash() []byte { + return tree.ImmutableTree.Hash() +} + +func (tree *MutableTree) WorkingVersion() int64 { + version := tree.version + 1 + if version == 1 && tree.ndb.opts.InitialVersion > 0 { + version = int64(tree.ndb.opts.InitialVersion) + } + return version +} + +// String returns a string representation of the tree. +func (tree *MutableTree) String() (string, error) { + return tree.ndb.String() +} + +// Set sets a key in the working tree. Nil values are invalid. The given +// key/value byte slices must not be modified after this call, since they point +// to slices stored within IAVL. It returns true when an existing value was +// updated, while false means it was a new key. +func (tree *MutableTree) Set(key, value []byte) (updated bool, err error) { + updated, err = tree.set(key, value) + if err != nil { + return false, err + } + return updated, nil +} + +// Get returns the value of the specified key if it exists, or nil otherwise. +// The returned value must not be modified, since it may point to data stored within IAVL. +func (tree *MutableTree) Get(key []byte) ([]byte, error) { + if tree.root == nil { + return nil, nil + } + + if !tree.skipFastStorageUpgrade { + if fastNode, ok := tree.unsavedFastNodeAdditions.Load(ibytes.UnsafeBytesToStr(key)); ok { + return fastNode.(*fastnode.Node).GetValue(), nil + } + // check if node was deleted + if _, ok := tree.unsavedFastNodeRemovals.Load(string(key)); ok { + return nil, nil + } + } + + return tree.ImmutableTree.Get(key) +} + +// Import returns an importer for tree nodes previously exported by ImmutableTree.Export(), +// producing an identical IAVL tree. The caller must call Close() on the importer when done. +// +// version should correspond to the version that was initially exported. It must be greater than +// or equal to the highest ExportNode version number given. +// +// Import can only be called on an empty tree. It is the callers responsibility that no other +// modifications are made to the tree while importing. +func (tree *MutableTree) Import(version int64) (*Importer, error) { + return newImporter(tree, version) +} + +// Iterate iterates over all keys of the tree. The keys and values must not be modified, +// since they may point to data stored within IAVL. Returns true if stopped by callnack, false otherwise +func (tree *MutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool, err error) { + if tree.root == nil { + return false, nil + } + + if tree.skipFastStorageUpgrade { + return tree.ImmutableTree.Iterate(fn) + } + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + if err != nil { + return false, err + } + if !isFastCacheEnabled { + return tree.ImmutableTree.Iterate(fn) + } + + itr := NewUnsavedFastIterator(nil, nil, true, tree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals) + defer itr.Close() + for ; itr.Valid(); itr.Next() { + if fn(itr.Key(), itr.Value()) { + return true, nil + } + } + return false, nil +} + +// Iterator returns an iterator over the mutable tree. +// CONTRACT: no updates are made to the tree while an iterator is active. +func (tree *MutableTree) Iterator(start, end []byte, ascending bool) (dbm.Iterator, error) { + if !tree.skipFastStorageUpgrade { + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + if err != nil { + return nil, err + } + + if isFastCacheEnabled { + return NewUnsavedFastIterator(start, end, ascending, tree.ndb, tree.unsavedFastNodeAdditions, tree.unsavedFastNodeRemovals), nil + } + } + + return tree.ImmutableTree.Iterator(start, end, ascending) +} + +func (tree *MutableTree) set(key []byte, value []byte) (updated bool, err error) { + if value == nil { + return updated, fmt.Errorf("attempt to store nil value at key '%s'", key) + } + + if tree.ImmutableTree.root == nil { + if !tree.skipFastStorageUpgrade { + tree.addUnsavedAddition(key, fastnode.NewNode(key, value, tree.version+1)) + } + tree.ImmutableTree.root = NewNode(key, value) + return updated, nil + } + + tree.ImmutableTree.root, updated, err = tree.recursiveSet(tree.ImmutableTree.root, key, value) + return updated, err +} + +func (tree *MutableTree) recursiveSet(node *Node, key []byte, value []byte) ( + newSelf *Node, updated bool, err error, +) { + if node.isLeaf() { + return tree.recursiveSetLeaf(node, key, value) + } + node, err = node.clone(tree) + if err != nil { + return nil, false, err + } + + if bytes.Compare(key, node.key) < 0 { + node.leftNode, updated, err = tree.recursiveSet(node.leftNode, key, value) + if err != nil { + return nil, updated, err + } + } else { + node.rightNode, updated, err = tree.recursiveSet(node.rightNode, key, value) + if err != nil { + return nil, updated, err + } + } + + if updated { + return node, updated, nil + } + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, false, err + } + newNode, err := tree.balance(node) + if err != nil { + return nil, false, err + } + return newNode, updated, err +} + +func (tree *MutableTree) recursiveSetLeaf(node *Node, key []byte, value []byte) ( + newSelf *Node, updated bool, err error, +) { + version := tree.version + 1 + if !tree.skipFastStorageUpgrade { + tree.addUnsavedAddition(key, fastnode.NewNode(key, value, version)) + } + switch bytes.Compare(key, node.key) { + case -1: // setKey < leafKey + return &Node{ + key: node.key, + subtreeHeight: 1, + size: 2, + nodeKey: nil, + leftNode: NewNode(key, value), + rightNode: node, + }, false, nil + case 1: // setKey > leafKey + return &Node{ + key: key, + subtreeHeight: 1, + size: 2, + nodeKey: nil, + leftNode: node, + rightNode: NewNode(key, value), + }, false, nil + default: + return NewNode(key, value), true, nil + } +} + +// Remove removes a key from the working tree. The given key byte slice should not be modified +// after this call, since it may point to data stored inside IAVL. +func (tree *MutableTree) Remove(key []byte) ([]byte, bool, error) { + if tree.root == nil { + return nil, false, nil + } + newRoot, _, value, removed, err := tree.recursiveRemove(tree.root, key) + if err != nil { + return nil, false, err + } + if !removed { + return nil, false, nil + } + + if !tree.skipFastStorageUpgrade { + tree.addUnsavedRemoval(key) + } + + tree.root = newRoot + return value, true, nil +} + +// removes the node corresponding to the passed key and balances the tree. +// It returns: +// - the hash of the new node (or nil if the node is the one removed) +// - the node that replaces the orig. node after remove +// - new leftmost leaf key for tree after successfully removing 'key' if changed. +// - the removed value +func (tree *MutableTree) recursiveRemove(node *Node, key []byte) (newSelf *Node, newKey []byte, newValue []byte, removed bool, err error) { + tree.logger.Debug("recursiveRemove", "node", node, "key", key) + if node.isLeaf() { + if bytes.Equal(key, node.key) { + return nil, nil, node.value, true, nil + } + return node, nil, nil, false, nil + } + + node, err = node.clone(tree) + if err != nil { + return nil, nil, nil, false, err + } + + // node.key < key; we go to the left to find the key: + if bytes.Compare(key, node.key) < 0 { + newLeftNode, newKey, value, removed, err := tree.recursiveRemove(node.leftNode, key) + if err != nil { + return nil, nil, nil, false, err + } + + if !removed { + return node, nil, value, removed, nil + } + + if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, removed, nil + } + + node.leftNode = newLeftNode + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, nil, nil, false, err + } + node, err = tree.balance(node) + if err != nil { + return nil, nil, nil, false, err + } + + return node, newKey, value, removed, nil + } + // node.key >= key; either found or look to the right: + newRightNode, newKey, value, removed, err := tree.recursiveRemove(node.rightNode, key) + if err != nil { + return nil, nil, nil, false, err + } + + if !removed { + return node, nil, value, removed, nil + } + + if newRightNode == nil { // right node held value, was removed + return node.leftNode, nil, value, removed, nil + } + + node.rightNode = newRightNode + if newKey != nil { + node.key = newKey + } + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, nil, nil, false, err + } + + node, err = tree.balance(node) + if err != nil { + return nil, nil, nil, false, err + } + + return node, nil, value, removed, nil +} + +// Load the latest versioned tree from disk. +func (tree *MutableTree) Load() (int64, error) { + return tree.LoadVersion(int64(0)) +} + +// Returns the version number of the specific version found +func (tree *MutableTree) LoadVersion(targetVersion int64) (int64, error) { + firstVersion, err := tree.ndb.getFirstVersion() + if err != nil { + return 0, err + } + + if firstVersion > 0 && firstVersion < int64(tree.ndb.opts.InitialVersion) { + return firstVersion, fmt.Errorf("initial version set to %v, but found earlier version %v", + tree.ndb.opts.InitialVersion, firstVersion) + } + + latestVersion, err := tree.ndb.getLatestVersion() + if err != nil { + return 0, err + } + + if firstVersion > 0 && firstVersion < int64(tree.ndb.opts.InitialVersion) { + return latestVersion, fmt.Errorf("initial version set to %v, but found earlier version %v", + tree.ndb.opts.InitialVersion, firstVersion) + } + + if latestVersion < targetVersion { + return latestVersion, fmt.Errorf("wanted to load target %d but only found up to %d", targetVersion, latestVersion) + } + + if firstVersion == 0 { + if targetVersion <= 0 { + if !tree.skipFastStorageUpgrade { + tree.mtx.Lock() + defer tree.mtx.Unlock() + _, err := tree.enableFastStorageAndCommitIfNotEnabled() + return 0, err + } + return 0, nil + } + return 0, fmt.Errorf("no versions found while trying to load %v", targetVersion) + } + + if targetVersion <= 0 { + targetVersion = latestVersion + } + if !tree.VersionExists(targetVersion) { + return 0, ErrVersionDoesNotExist + } + rootNodeKey, err := tree.ndb.GetRoot(targetVersion) + if err != nil { + return 0, err + } + + iTree := &ImmutableTree{ + ndb: tree.ndb, + version: targetVersion, + skipFastStorageUpgrade: tree.skipFastStorageUpgrade, + } + + if rootNodeKey != nil { + iTree.root, err = tree.ndb.GetNode(rootNodeKey) + if err != nil { + return 0, err + } + } + + tree.ImmutableTree = iTree + tree.lastSaved = iTree.clone() + + if !tree.skipFastStorageUpgrade { + // Attempt to upgrade + if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil { + return 0, err + } + } + + return latestVersion, nil +} + +// loadVersionForOverwriting attempts to load a tree at a previously committed +// version, or the latest version below it. Any versions greater than targetVersion will be deleted. +func (tree *MutableTree) LoadVersionForOverwriting(targetVersion int64) error { + if _, err := tree.LoadVersion(targetVersion); err != nil { + return err + } + + if err := tree.ndb.DeleteVersionsFrom(targetVersion + 1); err != nil { + return err + } + + // Commit the tree rollback first + // The fast storage rebuild don't have to be atomic with this, + // because it's idempotent and will do again when `LoadVersion`. + if err := tree.ndb.Commit(); err != nil { + return err + } + + if !tree.skipFastStorageUpgrade { + // it'll repopulates the fast node index because of version mismatch. + if _, err := tree.enableFastStorageAndCommitIfNotEnabled(); err != nil { + return err + } + } + + return nil +} + +// Returns true if the tree may be auto-upgraded, false otherwise +// An example of when an upgrade may be performed is when we are enaling fast storage for the first time or +// need to overwrite fast nodes due to mismatch with live state. +func (tree *MutableTree) IsUpgradeable() (bool, error) { + shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() + if err != nil { + return false, err + } + return !tree.skipFastStorageUpgrade && (!tree.ndb.hasUpgradedToFastStorage() || shouldForce), nil +} + +// enableFastStorageAndCommitIfNotEnabled if nodeDB doesn't mark fast storage as enabled, enable it, and commit the update. +// Checks whether the fast cache on disk matches latest live state. If not, deletes all existing fast nodes and repopulates them +// from latest tree. + +func (tree *MutableTree) enableFastStorageAndCommitIfNotEnabled() (bool, error) { + isUpgradeable, err := tree.IsUpgradeable() + if err != nil { + return false, err + } + + if !isUpgradeable { + return false, nil + } + + // If there is a mismatch between which fast nodes are on disk and the live state due to temporary + // downgrade and subsequent re-upgrade, we cannot know for sure which fast nodes have been removed while downgraded, + // Therefore, there might exist stale fast nodes on disk. As a result, to avoid persisting the stale state, it might + // be worth to delete the fast nodes from disk. + fastItr := NewFastIterator(nil, nil, true, tree.ndb) + defer fastItr.Close() + var deletedFastNodes uint64 + for ; fastItr.Valid(); fastItr.Next() { + deletedFastNodes++ + if err := tree.ndb.DeleteFastNode(fastItr.Key()); err != nil { + return false, err + } + } + + if err := tree.enableFastStorageAndCommit(); err != nil { + tree.ndb.storageVersion = defaultStorageVersionValue + return false, err + } + return true, nil +} + +func (tree *MutableTree) enableFastStorageAndCommit() error { + var err error + + itr := NewIterator(nil, nil, true, tree.ImmutableTree) + defer itr.Close() + var upgradedFastNodes uint64 + for ; itr.Valid(); itr.Next() { + upgradedFastNodes++ + if err = tree.ndb.SaveFastNodeNoCache(fastnode.NewNode(itr.Key(), itr.Value(), tree.version)); err != nil { + return err + } + } + + if err = itr.Error(); err != nil { + return err + } + + latestVersion, err := tree.ndb.getLatestVersion() + if err != nil { + return err + } + + if err = tree.ndb.SetFastStorageVersionToBatch(latestVersion); err != nil { + return err + } + + return tree.ndb.Commit() +} + +// GetImmutable loads an ImmutableTree at a given version for querying. The returned tree is +// safe for concurrent access, provided the version is not deleted, e.g. via `DeleteVersion()`. +func (tree *MutableTree) GetImmutable(version int64) (*ImmutableTree, error) { + rootNodeKey, err := tree.ndb.GetRoot(version) + if err != nil { + return nil, err + } + + var root *Node + if rootNodeKey != nil { + root, err = tree.ndb.GetNode(rootNodeKey) + if err != nil { + return nil, err + } + } + + return &ImmutableTree{ + root: root, + ndb: tree.ndb, + version: version, + skipFastStorageUpgrade: tree.skipFastStorageUpgrade, + }, nil +} + +// Rollback resets the working tree to the latest saved version, discarding +// any unsaved modifications. +func (tree *MutableTree) Rollback() { + if tree.version > 0 { + tree.ImmutableTree = tree.lastSaved.clone() + } else { + tree.ImmutableTree = &ImmutableTree{ + ndb: tree.ndb, + version: 0, + skipFastStorageUpgrade: tree.skipFastStorageUpgrade, + } + } + if !tree.skipFastStorageUpgrade { + tree.unsavedFastNodeAdditions = &sync.Map{} + tree.unsavedFastNodeRemovals = &sync.Map{} + } +} + +// GetVersioned gets the value at the specified key and version. The returned value must not be +// modified, since it may point to data stored within IAVL. +func (tree *MutableTree) GetVersioned(key []byte, version int64) ([]byte, error) { + if tree.VersionExists(version) { + if !tree.skipFastStorageUpgrade { + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + if err != nil { + return nil, err + } + + if isFastCacheEnabled { + fastNode, _ := tree.ndb.GetFastNode(key) + if fastNode == nil && version == tree.ndb.latestVersion { + return nil, nil + } + + if fastNode != nil && fastNode.GetVersionLastUpdatedAt() <= version { + return fastNode.GetValue(), nil + } + } + } + t, err := tree.GetImmutable(version) + if err != nil { + return nil, nil + } + value, err := t.Get(key) + if err != nil { + return nil, err + } + return value, nil + } + return nil, nil +} + +// SaveVersion saves a new tree version to disk, based on the current state of +// the tree. Returns the hash and new version number. +func (tree *MutableTree) SaveVersion() ([]byte, int64, error) { + version := tree.WorkingVersion() + + if tree.VersionExists(version) { + // If the version already exists, return an error as we're attempting to overwrite. + // However, the same hash means idempotent (i.e. no-op). + existingNodeKey, err := tree.ndb.GetRoot(version) + if err != nil { + return nil, version, err + } + var existingRoot *Node + if existingNodeKey != nil { + existingRoot, err = tree.ndb.GetNode(existingNodeKey) + if err != nil { + return nil, version, err + } + } + + newHash := tree.WorkingHash() + + if (existingRoot == nil && tree.root == nil) || (existingRoot != nil && bytes.Equal(existingRoot.hash, newHash)) { // TODO with WorkingHash + tree.version = version + tree.root = existingRoot + tree.ImmutableTree = tree.ImmutableTree.clone() + tree.lastSaved = tree.ImmutableTree.clone() + return newHash, version, nil + } + + return nil, version, fmt.Errorf("version %d was already saved to different hash from %X (existing nodeKey %d)", version, newHash, existingNodeKey) + } + + tree.logger.Debug("SAVE TREE", "version", version) + + // save new fast nodes + if !tree.skipFastStorageUpgrade { + if err := tree.saveFastNodeVersion(version); err != nil { + return nil, version, err + } + } + // save new nodes + if tree.root == nil { + if err := tree.ndb.SaveEmptyRoot(version); err != nil { + return nil, 0, err + } + } else { + if tree.root.nodeKey != nil { + // it means there are no updated nodes + if err := tree.ndb.SaveRoot(version, tree.root.nodeKey); err != nil { + return nil, 0, err + } + // it means the reference node is a legacy node + if tree.root.isLegacy { + // it will update the legacy node to the new format + // which ensures the reference node is not a legacy node + tree.root.isLegacy = false + if err := tree.ndb.SaveNode(tree.root); err != nil { + return nil, 0, fmt.Errorf("failed to save the reference legacy node: %w", err) + } + } + } else { + if err := tree.saveNewNodes(version); err != nil { + return nil, 0, err + } + } + } + + if err := tree.ndb.Commit(); err != nil { + return nil, version, err + } + + tree.ndb.resetLatestVersion(version) + tree.version = version + + // set new working tree + tree.ImmutableTree = tree.ImmutableTree.clone() + tree.lastSaved = tree.ImmutableTree.clone() + if !tree.skipFastStorageUpgrade { + tree.unsavedFastNodeAdditions = &sync.Map{} + tree.unsavedFastNodeRemovals = &sync.Map{} + } + + return tree.Hash(), version, nil +} + +func (tree *MutableTree) saveFastNodeVersion(latestVersion int64) error { + if err := tree.saveFastNodeAdditions(); err != nil { + return err + } + if err := tree.saveFastNodeRemovals(); err != nil { + return err + } + return tree.ndb.SetFastStorageVersionToBatch(latestVersion) +} + +func (tree *MutableTree) getUnsavedFastNodeAdditions() map[string]*fastnode.Node { + additions := make(map[string]*fastnode.Node) + tree.unsavedFastNodeAdditions.Range(func(key, value interface{}) bool { + additions[key.(string)] = value.(*fastnode.Node) + return true + }) + return additions +} + +// getUnsavedFastNodeRemovals returns unsaved FastNodes to remove + +func (tree *MutableTree) getUnsavedFastNodeRemovals() map[string]interface{} { + removals := make(map[string]interface{}) + tree.unsavedFastNodeRemovals.Range(func(key, value interface{}) bool { + removals[key.(string)] = value + return true + }) + return removals +} + +// addUnsavedAddition stores an addition into the unsaved additions map +func (tree *MutableTree) addUnsavedAddition(key []byte, node *fastnode.Node) { + skey := ibytes.UnsafeBytesToStr(key) + tree.unsavedFastNodeRemovals.Delete(skey) + tree.unsavedFastNodeAdditions.Store(skey, node) +} + +func (tree *MutableTree) saveFastNodeAdditions() error { + keysToSort := make([]string, 0) + tree.unsavedFastNodeAdditions.Range(func(k, v interface{}) bool { + keysToSort = append(keysToSort, k.(string)) + return true + }) + sort.Strings(keysToSort) + + for _, key := range keysToSort { + val, _ := tree.unsavedFastNodeAdditions.Load(key) + if err := tree.ndb.SaveFastNode(val.(*fastnode.Node)); err != nil { + return err + } + } + return nil +} + +// addUnsavedRemoval adds a removal to the unsaved removals map +func (tree *MutableTree) addUnsavedRemoval(key []byte) { + skey := ibytes.UnsafeBytesToStr(key) + tree.unsavedFastNodeAdditions.Delete(skey) + tree.unsavedFastNodeRemovals.Store(skey, true) +} + +func (tree *MutableTree) saveFastNodeRemovals() error { + keysToSort := make([]string, 0) + tree.unsavedFastNodeRemovals.Range(func(k, v interface{}) bool { + keysToSort = append(keysToSort, k.(string)) + return true + }) + sort.Strings(keysToSort) + + for _, key := range keysToSort { + if err := tree.ndb.DeleteFastNode(ibytes.UnsafeStrToBytes(key)); err != nil { + return err + } + } + return nil +} + +// SetInitialVersion sets the initial version of the tree, replacing Options.InitialVersion. +// It is only used during the initial SaveVersion() call for a tree with no other versions, +// and is otherwise ignored. +func (tree *MutableTree) SetInitialVersion(version uint64) { + tree.ndb.opts.InitialVersion = version +} + +// DeleteVersionsTo removes versions upto the given version from the MutableTree. +// It will not block the SaveVersion() call, instead it will be queued and executed deferred. +func (tree *MutableTree) DeleteVersionsTo(toVersion int64) error { + if err := tree.ndb.DeleteVersionsTo(toVersion); err != nil { + return err + } + + return tree.ndb.Commit() +} + +// Rotate right and return the new node and orphan. +func (tree *MutableTree) rotateRight(node *Node) (*Node, error) { + var err error + // TODO: optimize balance & rotate. + node, err = node.clone(tree) + if err != nil { + return nil, err + } + + newNode, err := node.leftNode.clone(tree) + if err != nil { + return nil, err + } + + node.leftNode = newNode.rightNode + newNode.rightNode = node + + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, err + } + + err = newNode.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, err + } + + return newNode, nil +} + +// Rotate left and return the new node and orphan. +func (tree *MutableTree) rotateLeft(node *Node) (*Node, error) { + var err error + // TODO: optimize balance & rotate. + node, err = node.clone(tree) + if err != nil { + return nil, err + } + + newNode, err := node.rightNode.clone(tree) + if err != nil { + return nil, err + } + + node.rightNode = newNode.leftNode + newNode.leftNode = node + + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, err + } + + err = newNode.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, err + } + + return newNode, nil +} + +// NOTE: assumes that node can be modified +// TODO: optimize balance & rotate +func (tree *MutableTree) balance(node *Node) (newSelf *Node, err error) { + if node.nodeKey != nil { + return nil, fmt.Errorf("unexpected balance() call on persisted node") + } + balance, err := node.calcBalance(tree.ImmutableTree) + if err != nil { + return nil, err + } + + if balance > 1 { + lftBalance, err := node.leftNode.calcBalance(tree.ImmutableTree) + if err != nil { + return nil, err + } + + if lftBalance >= 0 { + // Left Left Case + newNode, err := tree.rotateRight(node) + if err != nil { + return nil, err + } + return newNode, nil + } + // Left Right Case + node.leftNodeKey = nil + node.leftNode, err = tree.rotateLeft(node.leftNode) + if err != nil { + return nil, err + } + + newNode, err := tree.rotateRight(node) + if err != nil { + return nil, err + } + + return newNode, nil + } + if balance < -1 { + rightNode, err := node.getRightNode(tree.ImmutableTree) + if err != nil { + return nil, err + } + + rightBalance, err := rightNode.calcBalance(tree.ImmutableTree) + if err != nil { + return nil, err + } + if rightBalance <= 0 { + // Right Right Case + newNode, err := tree.rotateLeft(node) + if err != nil { + return nil, err + } + return newNode, nil + } + // Right Left Case + node.rightNodeKey = nil + node.rightNode, err = tree.rotateRight(rightNode) + if err != nil { + return nil, err + } + newNode, err := tree.rotateLeft(node) + if err != nil { + return nil, err + } + return newNode, nil + } + // Nothing changed + return node, nil +} + +// saveNewNodes save new created nodes by the changes of the working tree. +// NOTE: This function clears leftNode/rigthNode recursively and +// calls _hash() on the given node. +func (tree *MutableTree) saveNewNodes(version int64) error { + nonce := uint32(0) + newNodes := make([]*Node, 0) + var recursiveAssignKey func(*Node) ([]byte, error) + recursiveAssignKey = func(node *Node) ([]byte, error) { + if node.nodeKey != nil { + return node.GetKey(), nil + } + nonce++ + node.nodeKey = &NodeKey{ + version: version, + nonce: nonce, + } + + var err error + // the inner nodes should have two children. + if node.subtreeHeight > 0 { + node.leftNodeKey, err = recursiveAssignKey(node.leftNode) + if err != nil { + return nil, err + } + node.rightNodeKey, err = recursiveAssignKey(node.rightNode) + if err != nil { + return nil, err + } + } + + node._hash(version) + newNodes = append(newNodes, node) + + return node.nodeKey.GetKey(), nil + } + + if _, err := recursiveAssignKey(tree.root); err != nil { + return err + } + + for _, node := range newNodes { + if err := tree.ndb.SaveNode(node); err != nil { + return err + } + node.leftNode, node.rightNode = nil, nil + } + + return nil +} + +// SaveChangeSet saves a ChangeSet to the tree. +// It is used to replay a ChangeSet as a new version. +func (tree *MutableTree) SaveChangeSet(cs *ChangeSet) (int64, error) { + // if the tree has uncommitted changes, return error + if tree.root != nil && tree.root.nodeKey == nil { + return 0, fmt.Errorf("cannot save changeset with uncommitted changes") + } + for _, pair := range cs.Pairs { + if pair.Delete { + _, removed, err := tree.Remove(pair.Key) + if !removed { + return 0, fmt.Errorf("attempted to remove non-existent key %s", pair.Key) + } + if err != nil { + return 0, err + } + } else { + if _, err := tree.Set(pair.Key, pair.Value); err != nil { + return 0, err + } + } + } + _, version, err := tree.SaveVersion() + return version, err +} + +// Close closes the tree. +func (tree *MutableTree) Close() error { + tree.mtx.Lock() + defer tree.mtx.Unlock() + + tree.ImmutableTree = nil + tree.lastSaved = nil + return tree.ndb.Close() +} diff --git a/iavl/mutable_tree_test.go b/iavl/mutable_tree_test.go new file mode 100644 index 000000000..588b1dc47 --- /dev/null +++ b/iavl/mutable_tree_test.go @@ -0,0 +1,1452 @@ +package iavl + +import ( + "bytes" + "errors" + "fmt" + "runtime" + "sort" + "strconv" + "sync" + "testing" + + "cosmossdk.io/log" + "github.com/cosmos/iavl/fastnode" + + "github.com/cosmos/iavl/internal/encoding" + iavlrand "github.com/cosmos/iavl/internal/rand" + "github.com/cosmos/iavl/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +var ( + tKey1 = []byte("k1") + tVal1 = []byte("v1") + + tKey2 = []byte("k2") + tVal2 = []byte("v2") + // FIXME: enlarge maxIterator to 100000 + maxIterator = 100 +) + +func setupMutableTree(skipFastStorageUpgrade bool) *MutableTree { + memDB := dbm.NewMemDB() + tree := NewMutableTree(memDB, 0, skipFastStorageUpgrade, log.NewNopLogger()) + return tree +} + +// TestIterateConcurrency throws "fatal error: concurrent map writes" when fast node is enabled +func TestIterateConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupMutableTree(true) + wg := new(sync.WaitGroup) + for i := 0; i < 100; i++ { + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + tree.Iterate(func(key []byte, value []byte) bool { //nolint:errcheck + return false + }) + } + wg.Wait() +} + +// TestConcurrency throws "fatal error: concurrent map iteration and map write" and +// also sometimes "fatal error: concurrent map writes" when fast node is enabled +func TestIteratorConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupMutableTree(true) + _, err := tree.LoadVersion(0) + require.NoError(t, err) + // So much slower + wg := new(sync.WaitGroup) + for i := 0; i < 100; i++ { + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + itr, _ := tree.Iterator(nil, nil, true) + for ; itr.Valid(); itr.Next() { //nolint:revive + } // do nothing + } + wg.Wait() +} + +// TestNewIteratorConcurrency throws "fatal error: concurrent map writes" when fast node is enabled +func TestNewIteratorConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + tree := setupMutableTree(true) + for i := 0; i < 100; i++ { + wg := new(sync.WaitGroup) + it := NewIterator(nil, nil, true, tree.ImmutableTree) + for j := 0; j < maxIterator; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + _, err := tree.Set([]byte(fmt.Sprintf("%d%d", i, j)), iavlrand.RandBytes(1)) + require.NoError(t, err) + }(i, j) + } + for ; it.Valid(); it.Next() { //nolint:revive + } // do nothing + wg.Wait() + } +} + +func TestDelete(t *testing.T) { + tree := setupMutableTree(false) + + _, err := tree.set([]byte("k1"), []byte("Fred")) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + require.NoError(t, tree.DeleteVersionsTo(version)) + + proof, err := tree.GetVersionedProof([]byte("k1"), version) + require.EqualError(t, err, ErrVersionDoesNotExist.Error()) + require.Nil(t, proof) + + proof, err = tree.GetVersionedProof([]byte("k1"), version+1) + require.Nil(t, err) + require.Equal(t, 0, bytes.Compare([]byte("Fred"), proof.GetExist().Value)) +} + +func TestGetRemove(t *testing.T) { + require := require.New(t) + tree := setupMutableTree(false) + testGet := func(exists bool) { + v, err := tree.Get(tKey1) + require.NoError(err) + if exists { + require.Equal(tVal1, v, "key should exist") + } else { + require.Nil(v, "key should not exist") + } + } + + testGet(false) + + ok, err := tree.Set(tKey1, tVal1) + require.NoError(err) + require.False(ok, "new key set: nothing to update") + + // add second key to avoid tree.root removal + ok, err = tree.Set(tKey2, tVal2) + require.NoError(err) + require.False(ok, "new key set: nothing to update") + + testGet(true) + + // Save to tree.ImmutableTree + _, version, err := tree.SaveVersion() + require.NoError(err) + require.Equal(int64(1), version) + + testGet(true) + + v, ok, err := tree.Remove(tKey1) + require.NoError(err) + require.True(ok, "key should be removed") + require.Equal(tVal1, v, "key should exist") + + testGet(false) +} + +func TestTraverse(t *testing.T) { + tree := setupMutableTree(false) + + for i := 0; i < 6; i++ { + _, err := tree.set([]byte(fmt.Sprintf("k%d", i)), []byte(fmt.Sprintf("v%d", i))) + require.NoError(t, err) + } + + require.Equal(t, 11, tree.nodeSize(), "Size of tree unexpected") +} + +func TestMutableTree_DeleteVersionsTo(t *testing.T) { + tree := setupMutableTree(false) + + type entry struct { + key []byte + value []byte + } + + versionEntries := make(map[int64][]entry) + + // create 10 tree versions, each with 1000 random key/value entries + for i := 0; i < 10; i++ { + entries := make([]entry, 1000) + + for j := 0; j < 1000; j++ { + k := iavlrand.RandBytes(10) + v := iavlrand.RandBytes(10) + + entries[j] = entry{k, v} + _, err := tree.Set(k, v) + require.NoError(t, err) + } + + _, v, err := tree.SaveVersion() + require.NoError(t, err) + + versionEntries[v] = entries + } + + // delete even versions + versionToDelete := int64(8) + require.NoError(t, tree.DeleteVersionsTo(versionToDelete)) + + // ensure even versions have been deleted + for v := int64(1); v <= versionToDelete; v++ { + _, err := tree.LoadVersion(v) + require.Error(t, err) + } + + // ensure odd number versions exist and we can query for all set entries + for _, v := range []int64{9, 10} { + _, err := tree.LoadVersion(v) + require.NoError(t, err) + + for _, e := range versionEntries[v] { + val, err := tree.Get(e.key) + require.NoError(t, err) + if !bytes.Equal(e.value, val) { + t.Log(val) + } + // require.Equal(t, e.value, val) + } + } +} + +func TestMutableTree_LoadVersion_Empty(t *testing.T) { + tree := setupMutableTree(false) + + version, err := tree.LoadVersion(0) + require.NoError(t, err) + assert.EqualValues(t, 0, version) + + version, err = tree.LoadVersion(-1) + require.NoError(t, err) + assert.EqualValues(t, 0, version) + + _, err = tree.LoadVersion(3) + require.Error(t, err) +} + +func TestMutableTree_InitialVersion(t *testing.T) { + memDB := dbm.NewMemDB() + tree := NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) + + _, err := tree.Set([]byte("a"), []byte{0x01}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 9, version) + + _, err = tree.Set([]byte("b"), []byte{0x02}) + require.NoError(t, err) + _, version, err = tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + // Reloading the tree with the same initial version is fine + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) + version, err = tree.Load() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + // Reloading the tree with an initial version beyond the lowest should error + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(10)) + _, err = tree.Load() + require.Error(t, err) + + // Reloading the tree with a lower initial version is fine, and new versions can be produced + tree = NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(3)) + version, err = tree.Load() + require.NoError(t, err) + assert.EqualValues(t, 10, version) + + _, err = tree.Set([]byte("c"), []byte{0x03}) + require.NoError(t, err) + _, version, err = tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 11, version) +} + +func TestMutableTree_SetInitialVersion(t *testing.T) { + tree := setupMutableTree(false) + tree.SetInitialVersion(9) + + _, err := tree.Set([]byte("a"), []byte{0x01}) + require.NoError(t, err) + _, version, err := tree.SaveVersion() + require.NoError(t, err) + assert.EqualValues(t, 9, version) +} + +func BenchmarkMutableTree_Set(b *testing.B) { + db := dbm.NewMemDB() + t := NewMutableTree(db, 100000, false, log.NewNopLogger()) + for i := 0; i < 1000000; i++ { + _, err := t.Set(iavlrand.RandBytes(10), []byte{}) + require.NoError(b, err) + } + b.ReportAllocs() + runtime.GC() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := t.Set(iavlrand.RandBytes(10), []byte{}) + require.NoError(b, err) + } +} + +func prepareTree(t *testing.T) *MutableTree { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + for i := 0; i < 100; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte("a")) + require.NoError(t, err) + } + _, ver, err := tree.SaveVersion() + require.True(t, ver == 1) + require.NoError(t, err) + for i := 0; i < 100; i++ { + _, err = tree.Set([]byte{byte(i)}, []byte("b")) + require.NoError(t, err) + } + _, ver, err = tree.SaveVersion() + require.True(t, ver == 2) + require.NoError(t, err) + + newTree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + + return newTree +} + +func TestMutableTree_VersionExists(t *testing.T) { + tree := prepareTree(t) + require.True(t, tree.VersionExists(1)) + require.True(t, tree.VersionExists(2)) + require.False(t, tree.VersionExists(3)) +} + +func checkGetVersioned(t *testing.T, tree *MutableTree, version int64, key, value []byte) { + val, err := tree.GetVersioned(key, version) + require.NoError(t, err) + require.True(t, bytes.Equal(val, value)) +} + +func TestMutableTree_GetVersioned(t *testing.T) { + tree := prepareTree(t) + ver, err := tree.LoadVersion(1) + require.True(t, ver == 2) + require.NoError(t, err) + // check key of unloaded version + checkGetVersioned(t, tree, 1, []byte{1}, []byte("a")) + checkGetVersioned(t, tree, 2, []byte{1}, []byte("b")) + checkGetVersioned(t, tree, 3, []byte{1}, nil) + + tree = prepareTree(t) + ver, err = tree.LoadVersion(2) + require.True(t, ver == 2) + require.NoError(t, err) + checkGetVersioned(t, tree, 1, []byte{1}, []byte("a")) + checkGetVersioned(t, tree, 2, []byte{1}, []byte("b")) + checkGetVersioned(t, tree, 3, []byte{1}, nil) +} + +func TestMutableTree_DeleteVersion(t *testing.T) { + tree := prepareTree(t) + ver, err := tree.LoadVersion(2) + require.True(t, ver == 2) + require.NoError(t, err) + + require.NoError(t, tree.DeleteVersionsTo(1)) + + require.False(t, tree.VersionExists(1)) + require.True(t, tree.VersionExists(2)) + require.False(t, tree.VersionExists(3)) + + // cannot delete latest version + require.Error(t, tree.DeleteVersionsTo(2)) +} + +func TestMutableTree_LazyLoadVersionWithEmptyTree(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + _, v1, err := tree.SaveVersion() + require.NoError(t, err) + + newTree1 := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + v2, err := newTree1.LoadVersion(1) + require.NoError(t, err) + require.True(t, v1 == v2) + + newTree2 := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + v2, err = newTree1.LoadVersion(1) + require.NoError(t, err) + require.True(t, v1 == v2) + + require.True(t, newTree1.root == newTree2.root) +} + +func TestMutableTree_SetSimple(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 0, false, log.NewNopLogger()) + + const testKey1 = "a" + const testVal1 = "test" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + +func TestMutableTree_SetTwoKeys(t *testing.T) { + tree := setupMutableTree(false) + + const testKey1 = "a" + const testVal1 = "test" + + const testKey2 = "b" + const testVal2 = "test2" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + isUpdated, err = tree.Set([]byte(testKey2), []byte(testVal2)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastValue2, err := tree.Get([]byte(testKey2)) + require.NoError(t, err) + _, regularValue2, err := tree.GetWithIndex([]byte(testKey2)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue2) + require.Equal(t, []byte(testVal2), regularValue2) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 2, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) + + fastNodeAddition = fastNodeAdditions[testKey2] + require.Equal(t, []byte(testKey2), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal2), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + +func TestMutableTree_SetOverwrite(t *testing.T) { + tree := setupMutableTree(false) + const testKey1 = "a" + const testVal1 = "test" + const testVal2 = "test2" + + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + isUpdated, err = tree.Set([]byte(testKey1), []byte(testVal2)) + require.NoError(t, err) + require.True(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue) + require.Equal(t, []byte(testVal2), regularValue) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal2), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) +} + +func TestMutableTree_SetRemoveSet(t *testing.T) { + tree := setupMutableTree(false) + const testKey1 = "a" + const testVal1 = "test" + + // Set 1 + isUpdated, err := tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err := tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition := fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) + + // Remove + removedVal, isRemoved, err := tree.Remove([]byte(testKey1)) + require.NoError(t, err) + require.NotNil(t, removedVal) + require.True(t, isRemoved) + + fastNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, 0, len(fastNodeAdditions)) + + fastNodeRemovals := tree.getUnsavedFastNodeRemovals() + require.Equal(t, 1, len(fastNodeRemovals)) + + fastValue, err = tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Nil(t, fastValue) + require.Nil(t, regularValue) + + // Set 2 + isUpdated, err = tree.Set([]byte(testKey1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, isUpdated) + + fastValue, err = tree.Get([]byte(testKey1)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(testKey1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, 1, len(fastNodeAdditions)) + + fastNodeAddition = fastNodeAdditions[testKey1] + require.Equal(t, []byte(testKey1), fastNodeAddition.GetKey()) + require.Equal(t, []byte(testVal1), fastNodeAddition.GetValue()) + require.Equal(t, int64(1), fastNodeAddition.GetVersionLastUpdatedAt()) + + fastNodeRemovals = tree.getUnsavedFastNodeRemovals() + require.Equal(t, 0, len(fastNodeRemovals)) +} + +func TestMutableTree_FastNodeIntegration(t *testing.T) { + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 1000, false, log.NewNopLogger()) + + const key1 = "a" + const key2 = "b" + const key3 = "c" + + const testVal1 = "test" + const testVal2 = "test2" + + // Set key1 + res, err := tree.Set([]byte(key1), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions := tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 1) + + // Set key2 + res, err = tree.Set([]byte(key2), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + // Set key3 + res, err = tree.Set([]byte(key3), []byte(testVal1)) + require.NoError(t, err) + require.False(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Set key3 with new value + res, err = tree.Set([]byte(key3), []byte(testVal2)) + require.NoError(t, err) + require.True(t, res) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 3) + + // Remove key2 + removedVal, isRemoved, err := tree.Remove([]byte(key2)) + require.NoError(t, err) + require.True(t, isRemoved) + require.Equal(t, []byte(testVal1), removedVal) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 2) + + unsavedNodeRemovals := tree.getUnsavedFastNodeRemovals() + require.Equal(t, len(unsavedNodeRemovals), 1) + + // Save + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + unsavedNodeAdditions = tree.getUnsavedFastNodeAdditions() + require.Equal(t, len(unsavedNodeAdditions), 0) + + unsavedNodeRemovals = tree.getUnsavedFastNodeRemovals() + require.Equal(t, len(unsavedNodeRemovals), 0) + + // Load + t2 := NewMutableTree(mdb, 0, false, log.NewNopLogger()) + + _, err = t2.Load() + require.NoError(t, err) + + // Get and GetFast + fastValue, err := t2.Get([]byte(key1)) + require.NoError(t, err) + _, regularValue, err := tree.GetWithIndex([]byte(key1)) + require.NoError(t, err) + require.Equal(t, []byte(testVal1), fastValue) + require.Equal(t, []byte(testVal1), regularValue) + + fastValue, err = t2.Get([]byte(key2)) + require.NoError(t, err) + _, regularValue, err = t2.GetWithIndex([]byte(key2)) + require.NoError(t, err) + require.Nil(t, fastValue) + require.Nil(t, regularValue) + + fastValue, err = t2.Get([]byte(key3)) + require.NoError(t, err) + _, regularValue, err = tree.GetWithIndex([]byte(key3)) + require.NoError(t, err) + require.Equal(t, []byte(testVal2), fastValue) + require.Equal(t, []byte(testVal2), regularValue) +} + +func TestIterate_MutableTree_Unsaved(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + assertMutableMirrorIterate(t, tree, mirror) +} + +func TestIterate_MutableTree_Saved(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + assertMutableMirrorIterate(t, tree, mirror) +} + +func TestIterate_MutableTree_Unsaved_NextVersion(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + assertMutableMirrorIterate(t, tree, mirror) + + randomizeTreeAndMirror(t, tree, mirror) + + assertMutableMirrorIterate(t, tree, mirror) +} + +func TestIterator_MutableTree_Invalid(t *testing.T) { + tree := getTestTree(0) + + itr, err := tree.Iterator([]byte("a"), []byte("b"), true) + require.NoError(t, err) + require.NotNil(t, itr) + require.False(t, itr.Valid()) +} + +func TestUpgradeStorageToFast_LatestVersion_Success(t *testing.T) { + // Setup + db := dbm.NewMemDB() + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Default version when storage key does not exist in the db + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + mirror := make(map[string]string) + // Fill with some data + randomizeTreeAndMirror(t, tree, mirror) + + // Enable fast storage + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.True(t, enabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) +} + +func TestUpgradeStorageToFast_AlreadyUpgraded_Success(t *testing.T) { + // Setup + db := dbm.NewMemDB() + tree := NewMutableTree(db, 1000, false, log.NewNopLogger()) + + // Default version when storage key does not exist in the db + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + mirror := make(map[string]string) + // Fill with some data + randomizeTreeAndMirror(t, tree, mirror) + + // Enable fast storage + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.True(t, enabled) + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Test enabling fast storage when already enabled + enabled, err = tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) +} + +func TestUpgradeStorageToFast_DbErrorConstructor_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + expectedError := errors.New("some db error") + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, expectedError).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) + + tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + require.NotNil(t, tree) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) +} + +func TestUpgradeStorageToFast_DbErrorEnableFastStorage_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + expectedError := errors.New("some db error") + + batchMock := mock.NewMockBatch(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) + + iterMock := mock.NewMockIterator(ctrl) + dbMock.EXPECT().Iterator(gomock.Any(), gomock.Any()).Return(iterMock, nil) + iterMock.EXPECT().Error() + iterMock.EXPECT().Valid().Times(2) + iterMock.EXPECT().Close() + + batchMock.EXPECT().Set(gomock.Any(), gomock.Any()).Return(expectedError).Times(1) + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(1) + + tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + require.NotNil(t, tree) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.ErrorIs(t, err, expectedError) + require.False(t, enabled) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) +} + +func TestFastStorageReUpgradeProtection_NoForceUpgrade_Success(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // We are trying to test downgrade and re-upgrade protection + // We need to set up a state where latest fast storage version is equal to latest tree version + const latestFastStorageVersionOnDisk = 1 + const latestTreeVersion = latestFastStorageVersionOnDisk + + // Setup fake reverse iterator db to traverse root versions, called by ndb's getLatestVersion + expectedStorageVersion := []byte(fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(latestFastStorageVersionOnDisk)) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(1))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + batchMock := mock.NewMockBatch(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + + tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + require.NotNil(t, tree) + + // Pretend that we called Load and have the latest state in the tree + tree.version = latestTreeVersion + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, latestVersion, int64(latestTreeVersion)) + + // Ensure that the right branch of enableFastStorageAndCommitIfNotEnabled will be triggered + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) + + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) +} + +func TestFastStorageReUpgradeProtection_ForceUpgradeFirstTime_NoForceSecondTime_Success(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + iterMock := mock.NewMockIterator(ctrl) + rIterMock := mock.NewMockIterator(ctrl) + + // We are trying to test downgrade and re-upgrade protection + // We need to set up a state where latest fast storage version is of a lower version + // than tree version + const latestFastStorageVersionOnDisk = 1 + const latestTreeVersion = latestFastStorageVersionOnDisk + 1 + + // Setup db for iterator and reverse iterator mocks + expectedStorageVersion := []byte(fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(latestFastStorageVersionOnDisk)) + + // Setup fake reverse iterator db to traverse root versions, called by ndb's getLatestVersion + // rItr, err := db.ReverseIterator(rootKeyFormat.Key(1), rootKeyFormat.Key(latestTreeVersion + 1)) + // require.NoError(t, err) + + // dbMock represents the underlying database under the hood of nodeDB + dbMock.EXPECT().Get(gomock.Any()).Return(expectedStorageVersion, nil).Times(1) + + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(2) + dbMock.EXPECT().ReverseIterator(gomock.Any(), gomock.Any()).Return(rIterMock, nil).Times(1) // called to get latest version + startFormat := fastKeyFormat.Key() + endFormat := fastKeyFormat.Key() + endFormat[0]++ + dbMock.EXPECT().Iterator(startFormat, endFormat).Return(iterMock, nil).Times(1) + + // rIterMock is used to get the latest version from disk. We are mocking that rIterMock returns latestTreeVersion from disk + rIterMock.EXPECT().Valid().Return(true).Times(1) + rIterMock.EXPECT().Key().Return(nodeKeyFormat.Key(GetRootKey(latestTreeVersion))) + rIterMock.EXPECT().Close().Return(nil).Times(1) + + fastNodeKeyToDelete := []byte("some_key") + + // batchMock represents a structure that receives all the updates related to + // upgrade and then commits them all in the end. + updatedExpectedStorageVersion := make([]byte, len(expectedStorageVersion)) + copy(updatedExpectedStorageVersion, expectedStorageVersion) + updatedExpectedStorageVersion[len(updatedExpectedStorageVersion)-1]++ + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(2) + batchMock.EXPECT().Delete(fastKeyFormat.Key(fastNodeKeyToDelete)).Return(nil).Times(1) + batchMock.EXPECT().Set(metadataKeyFormat.Key([]byte(storageVersionKey)), updatedExpectedStorageVersion).Return(nil).Times(1) + batchMock.EXPECT().Write().Return(nil).Times(1) + batchMock.EXPECT().Close().Return(nil).Times(1) + + // iterMock is used to mock the underlying db iterator behing fast iterator + // Here, we want to mock the behavior of deleting fast nodes from disk when + // force upgrade is detected. + iterMock.EXPECT().Valid().Return(true).Times(1) + iterMock.EXPECT().Error().Return(nil).Times(1) + iterMock.EXPECT().Key().Return(fastKeyFormat.Key(fastNodeKeyToDelete)).Times(1) + // encode value + var buf bytes.Buffer + testValue := "test_value" + buf.Grow(encoding.EncodeVarintSize(int64(latestFastStorageVersionOnDisk)) + encoding.EncodeBytesSize([]byte(testValue))) + err := encoding.EncodeVarint(&buf, int64(latestFastStorageVersionOnDisk)) + require.NoError(t, err) + err = encoding.EncodeBytes(&buf, []byte(testValue)) + require.NoError(t, err) + iterMock.EXPECT().Value().Return(buf.Bytes()).Times(1) // this is encoded as version 1 with value "2" + iterMock.EXPECT().Valid().Return(true).Times(1) + // Call Next at the end of loop iteration + iterMock.EXPECT().Next().Return().Times(1) + iterMock.EXPECT().Error().Return(nil).Times(1) + iterMock.EXPECT().Valid().Return(false).Times(1) + // Call Valid after first iteraton + iterMock.EXPECT().Valid().Return(false).Times(1) + iterMock.EXPECT().Close().Return(nil).Times(1) + + tree := NewMutableTree(dbMock, 0, false, log.NewNopLogger()) + require.NotNil(t, tree) + + // Pretend that we called Load and have the latest state in the tree + tree.version = latestTreeVersion + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, latestVersion, int64(latestTreeVersion)) + + // Ensure that the right branch of enableFastStorageAndCommitIfNotEnabled will be triggered + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + shouldForce, err := tree.ndb.shouldForceFastStorageUpgrade() + require.True(t, shouldForce) + require.NoError(t, err) + + // Actual method under test + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.True(t, enabled) + + // Test that second time we call this, force upgrade does not happen + enabled, err = tree.enableFastStorageAndCommitIfNotEnabled() + require.NoError(t, err) + require.False(t, enabled) +} + +func TestUpgradeStorageToFast_Integration_Upgraded_FastIterator_Success(t *testing.T) { + // Setup + tree, mirror := setupTreeAndMirror(t, 100, false) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + + // Should auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewMutableTree(tree.ndb.db, 1000, false, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) // upgraded in save version + require.NoError(t, err) + + // Load version - should auto enable fast storage + version, err := sut.Load() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + require.Equal(t, int64(1), version) + + // Test that upgraded mutable tree iterates as expected + t.Run("Mutable tree", func(t *testing.T) { + i := 0 + sut.Iterate(func(k, v []byte) bool { //nolint:errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) + + // Test that upgraded immutable tree iterates as expected + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + i := 0 + immutableTree.Iterate(func(k, v []byte) bool { //nolint:errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) +} + +func TestUpgradeStorageToFast_Integration_Upgraded_GetFast_Success(t *testing.T) { + // Setup + tree, mirror := setupTreeAndMirror(t, 100, false) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.True(t, isUpgradeable) + require.NoError(t, err) + + // Should auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewMutableTree(tree.ndb.db, 1000, false, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) // upgraded in save version + require.NoError(t, err) + + // LazyLoadVersion - should auto enable fast storage + version, err := sut.LoadVersion(1) + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + require.Equal(t, int64(1), version) + + t.Run("Mutable tree", func(t *testing.T) { + for _, kv := range mirror { + v, err := sut.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) + + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + for _, kv := range mirror { + v, err := immutableTree.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) +} + +func TestUpgradeStorageToFast_Success(t *testing.T) { + commitGap := 1000 + + type fields struct { + nodeCount int + } + tests := []struct { + name string + fields fields + }{ + {"less than commit gap", fields{nodeCount: 100}}, + {"equal to commit gap", fields{nodeCount: commitGap}}, + {"great than commit gap", fields{nodeCount: commitGap + 100}}, + {"two times commit gap", fields{nodeCount: commitGap * 2}}, + {"two times plus commit gap", fields{nodeCount: commitGap*2 + 1}}, + } + + for _, tt := range tests { + tree, mirror := setupTreeAndMirror(t, tt.fields.nodeCount, false) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.Nil(t, err) + require.True(t, enabled) + t.Run(tt.name, func(t *testing.T) { + i := 0 + iter := NewFastIterator(nil, nil, true, tree.ndb) + for ; iter.Valid(); iter.Next() { + require.Equal(t, []byte(mirror[i][0]), iter.Key()) + require.Equal(t, []byte(mirror[i][1]), iter.Value()) + i++ + } + require.Equal(t, len(mirror), i) + }) + } +} + +func TestUpgradeStorageToFast_Delete_Stale_Success(t *testing.T) { + // we delete fast node, in case of deadlock. we should limit the stale count lower than chBufferSize(64) + commitGap := 5 + + valStale := "val_stale" + addStaleKey := func(ndb *nodeDB, staleCount int) { + keyPrefix := "key_prefix" + b := ndb.db.NewBatch() + for i := 0; i < staleCount; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + + node := fastnode.NewNode([]byte(key), []byte(valStale), 100) + var buf bytes.Buffer + buf.Grow(node.EncodedSize()) + err := node.WriteBytes(&buf) + require.NoError(t, err) + err = b.Set(ndb.fastNodeKey([]byte(key)), buf.Bytes()) + require.NoError(t, err) + } + require.NoError(t, b.Write()) + } + type fields struct { + nodeCount int + staleCount int + } + + tests := []struct { + name string + fields fields + }{ + {"stale less than commit gap", fields{nodeCount: 100, staleCount: 4}}, + {"stale equal to commit gap", fields{nodeCount: commitGap, staleCount: commitGap}}, + {"stale great than commit gap", fields{nodeCount: commitGap + 100, staleCount: commitGap*2 - 1}}, + {"stale twice commit gap", fields{nodeCount: commitGap + 100, staleCount: commitGap * 2}}, + {"stale great than twice commit gap", fields{nodeCount: commitGap, staleCount: commitGap*2 + 1}}, + } + + for _, tt := range tests { + tree, mirror := setupTreeAndMirror(t, tt.fields.nodeCount, false) + addStaleKey(tree.ndb, tt.fields.staleCount) + enabled, err := tree.enableFastStorageAndCommitIfNotEnabled() + require.Nil(t, err) + require.True(t, enabled) + t.Run(tt.name, func(t *testing.T) { + i := 0 + iter := NewFastIterator(nil, nil, true, tree.ndb) + for ; iter.Valid(); iter.Next() { + require.Equal(t, []byte(mirror[i][0]), iter.Key()) + require.Equal(t, []byte(mirror[i][1]), iter.Value()) + i++ + } + require.Equal(t, len(mirror), i) + }) + } +} + +func setupTreeAndMirror(t *testing.T, numEntries int, skipFastStorageUpgrade bool) (*MutableTree, [][]string) { + db := dbm.NewMemDB() + + tree := NewMutableTree(db, 0, skipFastStorageUpgrade, log.NewNopLogger()) + + keyPrefix, valPrefix := "key", "val" + + mirror := make([][]string, 0, numEntries) + for i := 0; i < numEntries; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + val := fmt.Sprintf("%s_%d", valPrefix, i) + mirror = append(mirror, []string{key, val}) + updated, err := tree.Set([]byte(key), []byte(val)) + require.False(t, updated) + require.NoError(t, err) + } + + // Delete fast nodes from database to mimic a version with no upgrade + for i := 0; i < numEntries; i++ { + key := fmt.Sprintf("%s_%d", keyPrefix, i) + require.NoError(t, db.Delete(fastKeyFormat.Key([]byte(key)))) + } + + sort.Slice(mirror, func(i, j int) bool { + return mirror[i][0] < mirror[j][0] + }) + return tree, mirror +} + +func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Get_Success(t *testing.T) { + // Setup + tree, mirror := setupTreeAndMirror(t, 100, true) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Should Not auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewMutableTree(tree.ndb.db, 1000, true, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // LazyLoadVersion - should not auto enable fast storage + version, err := sut.LoadVersion(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Load - should not auto enable fast storage + version, err = sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // LoadVersion - should not auto enable fast storage + version, err = sut.LoadVersion(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // LoadVersionForOverwriting - should not auto enable fast storage + err = sut.LoadVersionForOverwriting(1) + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + t.Run("Mutable tree", func(t *testing.T) { + for _, kv := range mirror { + v, err := sut.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) + + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + for _, kv := range mirror { + v, err := immutableTree.Get([]byte(kv[0])) + require.NoError(t, err) + require.Equal(t, []byte(kv[1]), v) + } + }) +} + +func TestNoFastStorageUpgrade_Integration_SaveVersion_Load_Iterate_Success(t *testing.T) { + // Setup + tree, mirror := setupTreeAndMirror(t, 100, true) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err := tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Should Not auto enable in save version + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = tree.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + sut := NewMutableTree(tree.ndb.db, 1000, true, log.NewNopLogger()) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + isUpgradeable, err = sut.IsUpgradeable() + require.False(t, isUpgradeable) + require.NoError(t, err) + + // Load - should not auto enable fast storage + version, err := sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = sut.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Load - should not auto enable fast storage + version, err = sut.Load() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + isFastCacheEnabled, err = tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + // Test that the mutable tree iterates as expected + t.Run("Mutable tree", func(t *testing.T) { + i := 0 + sut.Iterate(func(k, v []byte) bool { //nolint: errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) + + // Test that the immutable tree iterates as expected + t.Run("Immutable tree", func(t *testing.T) { + immutableTree, err := sut.GetImmutable(sut.version) + require.NoError(t, err) + + i := 0 + immutableTree.Iterate(func(k, v []byte) bool { //nolint: errcheck + require.Equal(t, []byte(mirror[i][0]), k) + require.Equal(t, []byte(mirror[i][1]), v) + i++ + return false + }) + }) +} + +// TestMutableTree_InitialVersion_FirstVersion demonstrate the un-intuitive behavior, +// when InitialVersion is set the nodes created in the first version are not assigned with expected version number. +func TestMutableTree_InitialVersion_FirstVersion(t *testing.T) { + db := dbm.NewMemDB() + + initialVersion := int64(1000) + tree := NewMutableTree(db, 0, true, log.NewNopLogger(), InitialVersionOption(uint64(initialVersion))) + + _, err := tree.Set([]byte("hello"), []byte("world")) + require.NoError(t, err) + + _, version, err := tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, initialVersion, version) + rootKey := GetRootKey(version) + // the nodes created at the first version are not assigned with the `InitialVersion` + node, err := tree.ndb.GetNode(rootKey) + require.NoError(t, err) + require.Equal(t, initialVersion, node.nodeKey.version) + + _, err = tree.Set([]byte("hello"), []byte("world1")) + require.NoError(t, err) + + _, version, err = tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, initialVersion+1, version) + rootKey = GetRootKey(version) + // the following versions behaves normally + node, err = tree.ndb.GetNode(rootKey) + require.NoError(t, err) + require.Equal(t, initialVersion+1, node.nodeKey.version) +} + +func TestMutableTreeClose(t *testing.T) { + db := dbm.NewMemDB() + tree := NewMutableTree(db, 0, true, log.NewNopLogger()) + + _, err := tree.Set([]byte("hello"), []byte("world")) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + require.NoError(t, tree.Close()) +} diff --git a/iavl/node.go b/iavl/node.go new file mode 100644 index 000000000..13749af8b --- /dev/null +++ b/iavl/node.go @@ -0,0 +1,763 @@ +package iavl + +// NOTE: This file favors int64 as opposed to int for size/counts. +// The Tree on the other hand favors int. This is intentional. + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + + "github.com/cosmos/iavl/cache" + + "github.com/cosmos/iavl/internal/color" + "github.com/cosmos/iavl/internal/encoding" +) + +const ( + // ModeLegacyLeftNode is the mode for legacy left child in the node encoding/decoding. + ModeLegacyLeftNode = 0x01 + // ModeLegacyRightNode is the mode for legacy right child in the node encoding/decoding. + ModeLegacyRightNode = 0x02 +) + +// NodeKey represents a key of node in the DB. +type NodeKey struct { + version int64 + nonce uint32 +} + +// GetKey returns a byte slice of the NodeKey. +func (nk *NodeKey) GetKey() []byte { + b := make([]byte, 12) + binary.BigEndian.PutUint64(b, uint64(nk.version)) + binary.BigEndian.PutUint32(b[8:], nk.nonce) + return b +} + +// GetNodeKey returns a NodeKey from a byte slice. +func GetNodeKey(key []byte) *NodeKey { + return &NodeKey{ + version: int64(binary.BigEndian.Uint64(key)), + nonce: binary.BigEndian.Uint32(key[8:]), + } +} + +// GetRootKey returns a byte slice of the root node key for the given version. +func GetRootKey(version int64) []byte { + b := make([]byte, 12) + binary.BigEndian.PutUint64(b, uint64(version)) + binary.BigEndian.PutUint32(b[8:], 1) + return b +} + +// Node represents a node in a Tree. +type Node struct { + key []byte + value []byte + hash []byte + nodeKey *NodeKey + // Legacy: LeftNodeHash + // v1: Left node ptr via Version/key + leftNodeKey []byte + // Legacy: RightNodeHash + // v1: Right node ptr via Version/key + rightNodeKey []byte + size int64 + leftNode *Node + rightNode *Node + subtreeHeight int8 + isLegacy bool +} + +var _ cache.Node = (*Node)(nil) + +// NewNode returns a new node from a key, value and version. +func NewNode(key []byte, value []byte) *Node { + return &Node{ + key: key, + value: value, + subtreeHeight: 0, + size: 1, + } +} + +// GetKey returns the key of the node. +func (node *Node) GetKey() []byte { + if node.isLegacy { + return node.hash + } + return node.nodeKey.GetKey() +} + +// MakeNode constructs an *Node from an encoded byte slice. +func MakeNode(nk, buf []byte) (*Node, error) { + // Read node header (height, size, key). + height, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.height, %w", err) + } + buf = buf[n:] + height8 := int8(height) + if height != int64(height8) { + return nil, errors.New("invalid height, out of int8 range") + } + + size, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.size, %w", err) + } + buf = buf[n:] + + key, n, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.key, %w", err) + } + buf = buf[n:] + + node := &Node{ + subtreeHeight: height8, + size: size, + nodeKey: GetNodeKey(nk), + key: key, + } + + // Read node body. + if node.isLeaf() { + val, _, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.value, %w", err) + } + node.value = val + // ensure take the hash for the leaf node + node._hash(node.nodeKey.version) + } else { // Read children. + node.hash, n, err = encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.hash, %w", err) + } + buf = buf[n:] + + mode, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding mode, %w", err) + } + buf = buf[n:] + if mode < 0 || mode > 3 { + return nil, errors.New("invalid mode") + } + + if mode&ModeLegacyLeftNode != 0 { // legacy leftNodeKey + node.leftNodeKey, n, err = encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding legacy node.leftNodeKey, %w", err) + } + buf = buf[n:] + } else { + var ( + leftNodeKey NodeKey + nonce int64 + ) + leftNodeKey.version, n, err = encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.leftNodeKey.version, %w", err) + } + buf = buf[n:] + nonce, n, err = encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.leftNodeKey.nonce, %w", err) + } + buf = buf[n:] + leftNodeKey.nonce = uint32(nonce) + if nonce != int64(leftNodeKey.nonce) { + return nil, errors.New("invalid leftNodeKey.nonce, out of int32 range") + } + node.leftNodeKey = leftNodeKey.GetKey() + } + if mode&ModeLegacyRightNode != 0 { // legacy rightNodeKey + node.rightNodeKey, _, err = encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding legacy node.rightNodeKey, %w", err) + } + } else { + var ( + rightNodeKey NodeKey + nonce int64 + ) + rightNodeKey.version, n, err = encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.rightNodeKey.version, %w", err) + } + buf = buf[n:] + nonce, _, err = encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.rightNodeKey.nonce, %w", err) + } + rightNodeKey.nonce = uint32(nonce) + if nonce != int64(rightNodeKey.nonce) { + return nil, errors.New("invalid rightNodeKey.nonce, out of int32 range") + } + node.rightNodeKey = rightNodeKey.GetKey() + } + } + return node, nil +} + +// MakeLegacyNode constructs a legacy *Node from an encoded byte slice. +func MakeLegacyNode(hash, buf []byte) (*Node, error) { + // Read node header (height, size, version, key). + height, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.height, %w", err) + } + buf = buf[n:] + if height < int64(math.MinInt8) || height > int64(math.MaxInt8) { + return nil, errors.New("invalid height, must be int8") + } + + size, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.size, %w", err) + } + buf = buf[n:] + + ver, n, err := encoding.DecodeVarint(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.version, %w", err) + } + buf = buf[n:] + + key, n, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.key, %w", err) + } + buf = buf[n:] + + node := &Node{ + subtreeHeight: int8(height), + size: size, + nodeKey: &NodeKey{version: ver}, + key: key, + hash: hash, + isLegacy: true, + } + + // Read node body. + + if node.isLeaf() { + val, _, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.value, %w", err) + } + node.value = val + } else { // Read children. + leftHash, n, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.leftHash, %w", err) + } + buf = buf[n:] + + rightHash, _, err := encoding.DecodeBytes(buf) + if err != nil { + return nil, fmt.Errorf("decoding node.rightHash, %w", err) + } + node.leftNodeKey = leftHash + node.rightNodeKey = rightHash + } + return node, nil +} + +// String returns a string representation of the node key. +func (nk *NodeKey) String() string { + return fmt.Sprintf("(%d, %d)", nk.version, nk.nonce) +} + +// String returns a string representation of the node. +func (node *Node) String() string { + child := "" + if node.leftNode != nil && node.leftNode.nodeKey != nil { + child += fmt.Sprintf("{left %v}", node.leftNode.nodeKey) + } + if node.rightNode != nil && node.rightNode.nodeKey != nil { + child += fmt.Sprintf("{right %v}", node.rightNode.nodeKey) + } + // scrt-FIX: use #v on nil values to avoid panic + return fmt.Sprintf("Node{%s:%s@ %#v:%#x-%#x %d-%d %x}#%s\n", + color.ColoredBytes(node.key, color.Green, color.Blue), + color.ColoredBytes(node.value, color.Cyan, color.Blue), + node.nodeKey, node.leftNodeKey, node.rightNodeKey, + node.size, node.subtreeHeight, node.hash, child) +} + +// clone creates a shallow copy of a node with its hash set to nil. +func (node *Node) clone(tree *MutableTree) (*Node, error) { + if node.isLeaf() { + return nil, ErrCloneLeafNode + } + + // ensure get children + var err error + leftNode := node.leftNode + rightNode := node.rightNode + if node.nodeKey != nil { + leftNode, err = node.getLeftNode(tree.ImmutableTree) + if err != nil { + return nil, err + } + rightNode, err = node.getRightNode(tree.ImmutableTree) + if err != nil { + return nil, err + } + node.leftNode = nil + node.rightNode = nil + } + + return &Node{ + key: node.key, + subtreeHeight: node.subtreeHeight, + size: node.size, + hash: nil, + nodeKey: nil, + leftNodeKey: node.leftNodeKey, + rightNodeKey: node.rightNodeKey, + leftNode: leftNode, + rightNode: rightNode, + }, nil +} + +func (node *Node) isLeaf() bool { + return node.subtreeHeight == 0 +} + +// Check if the node has a descendant with the given key. +func (node *Node) has(t *ImmutableTree, key []byte) (has bool, err error) { + if bytes.Equal(node.key, key) { + return true, nil + } + if node.isLeaf() { + return false, nil + } + if bytes.Compare(key, node.key) < 0 { + leftNode, err := node.getLeftNode(t) + if err != nil { + return false, err + } + return leftNode.has(t, key) + } + + rightNode, err := node.getRightNode(t) + if err != nil { + return false, err + } + + return rightNode.has(t, key) +} + +// Get a key under the node. +// +// The index is the index in the list of leaf nodes sorted lexicographically by key. The leftmost leaf has index 0. +// It's neighbor has index 1 and so on. +func (node *Node) get(t *ImmutableTree, key []byte) (index int64, value []byte, err error) { + if node.isLeaf() { + switch bytes.Compare(node.key, key) { + case -1: + return 1, nil, nil + case 1: + return 0, nil, nil + default: + return 0, node.value, nil + } + } + + if bytes.Compare(key, node.key) < 0 { + leftNode, err := node.getLeftNode(t) + if err != nil { + return 0, nil, err + } + + return leftNode.get(t, key) + } + + rightNode, err := node.getRightNode(t) + if err != nil { + return 0, nil, err + } + + index, value, err = rightNode.get(t, key) + if err != nil { + return 0, nil, err + } + + index += node.size - rightNode.size + return index, value, nil +} + +func (node *Node) getByIndex(t *ImmutableTree, index int64) (key []byte, value []byte, err error) { + if node.isLeaf() { + if index == 0 { + return node.key, node.value, nil + } + return nil, nil, nil + } + // TODO: could improve this by storing the + // sizes as well as left/right hash. + leftNode, err := node.getLeftNode(t) + if err != nil { + return nil, nil, err + } + + if index < leftNode.size { + return leftNode.getByIndex(t, index) + } + + rightNode, err := node.getRightNode(t) + if err != nil { + return nil, nil, err + } + + return rightNode.getByIndex(t, index-leftNode.size) +} + +// Computes the hash of the node without computing its descendants. Must be +// called on nodes which have descendant node hashes already computed. +func (node *Node) _hash(version int64) []byte { + if node.hash != nil { + return node.hash + } + + h := sha256.New() + if err := node.writeHashBytes(h, version); err != nil { + return nil + } + node.hash = h.Sum(nil) + + return node.hash +} + +// Hash the node and its descendants recursively. This usually mutates all +// descendant nodes. Returns the node hash and number of nodes hashed. +// If the tree is empty (i.e. the node is nil), returns the hash of an empty input, +// to conform with RFC-6962. +func (node *Node) hashWithCount(version int64) []byte { + if node == nil { + fmt.Printf("NODE IS NULL, HASH IS %+v\n", sha256.New().Sum(nil)) + return sha256.New().Sum(nil) + } + if node.hash != nil { + fmt.Printf("NODE HASH IS NOT NULL: %+v\n", node.hash) + return node.hash + } + + h := sha256.New() + if err := node.writeHashBytesRecursively(h, version); err != nil { + // writeHashBytesRecursively doesn't return an error unless h.Write does, + // and hash.Hash.Write doesn't. + panic(err) + } + node.hash = h.Sum(nil) + + fmt.Printf("FINAL NODE HASH IS: %+v\n", node.hash) + return node.hash +} + +// validate validates the node contents +func (node *Node) validate() error { + if node == nil { + return errors.New("node cannot be nil") + } + if node.key == nil { + return errors.New("key cannot be nil") + } + if node.nodeKey == nil { + return errors.New("nodeKey cannot be nil") + } + if node.nodeKey.version <= 0 { + return errors.New("version must be greater than 0") + } + if node.subtreeHeight < 0 { + return errors.New("height cannot be less than 0") + } + if node.size < 1 { + return errors.New("size must be at least 1") + } + + if node.subtreeHeight == 0 { + // Leaf nodes + if node.value == nil { + return errors.New("value cannot be nil for leaf node") + } + if node.leftNodeKey != nil || node.leftNode != nil || node.rightNodeKey != nil || node.rightNode != nil { + return errors.New("leaf node cannot have children") + } + if node.size != 1 { + return errors.New("leaf nodes must have size 1") + } + } else if node.value != nil { + return errors.New("value must be nil for non-leaf node") + } + return nil +} + +// Writes the node's hash to the given io.Writer. This function expects +// child hashes to be already set. +func (node *Node) writeHashBytes(w io.Writer, version int64) error { + err := encoding.EncodeVarint(w, int64(node.subtreeHeight)) + if err != nil { + return fmt.Errorf("writing height, %w", err) + } + err = encoding.EncodeVarint(w, node.size) + if err != nil { + return fmt.Errorf("writing size, %w", err) + } + err = encoding.EncodeVarint(w, version) + if err != nil { + return fmt.Errorf("writing version, %w", err) + } + fmt.Printf("WRITING HASH:\nHEIGHT: %d\nSIZE: %d\nVERSION: %d\n", node.subtreeHeight, node.size, version) + + // Key is not written for inner nodes, unlike writeBytes. + + if node.isLeaf() { + fmt.Println("NODE IS LEAF") + err = encoding.EncodeBytes(w, node.key) + if err != nil { + return fmt.Errorf("writing key, %w", err) + } + fmt.Printf("KEY: %+v\nVALUE: %+v\n", node.key, node.value) + + // Indirection needed to provide proofs without values. + // (e.g. ProofLeafNode.ValueHash) + valueHash := sha256.Sum256(node.value) + + err = encoding.Encode32BytesHash(w, valueHash[:]) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + if node.leftNode == nil || node.rightNode == nil { + return ErrEmptyChild + } + fmt.Println("NODE IS NOT LEAF") + + if err := encoding.Encode32BytesHash(w, node.leftNode.hash); err != nil { + return fmt.Errorf("writing left hash, %w", err) + } + + if err := encoding.Encode32BytesHash(w, node.rightNode.hash); err != nil { + return fmt.Errorf("writing right hash, %w", err) + } + fmt.Printf("LEFT CHILD HASH: %+v\nRIGHT CHILD HASH: %+v\n", node.leftNode.hash, node.rightNode.hash) + } + + return nil +} + +// writeHashBytesRecursively writes the node's hash to the given io.Writer. +// This function has the side-effect of calling hashWithCount. +// It only returns an error if w.Write fails. +func (node *Node) writeHashBytesRecursively(w io.Writer, version int64) error { + fmt.Println("LEFT:") + node.leftNode.hashWithCount(version) + fmt.Println("RIGHT:") + node.rightNode.hashWithCount(version) + return node.writeHashBytes(w, version) +} + +func (node *Node) encodedSize() int { + n := 1 + + encoding.EncodeVarintSize(node.size) + + encoding.EncodeBytesSize(node.key) + if node.isLeaf() { + n += encoding.EncodeBytesSize(node.value) + } else { + n += encoding.EncodeBytesSize(node.hash) + if node.leftNodeKey != nil { + nk := GetNodeKey(node.leftNodeKey) + n += encoding.EncodeVarintSize(nk.version) + + encoding.EncodeVarintSize(int64(nk.nonce)) + } + if node.rightNodeKey != nil { + nk := GetNodeKey(node.rightNodeKey) + n += encoding.EncodeVarintSize(nk.version) + + encoding.EncodeVarintSize(int64(nk.nonce)) + } + } + return n +} + +// Writes the node as a serialized byte slice to the supplied io.Writer. +func (node *Node) writeBytes(w io.Writer) error { + if node == nil { + return errors.New("cannot write nil node") + } + err := encoding.EncodeVarint(w, int64(node.subtreeHeight)) + if err != nil { + return fmt.Errorf("writing height, %w", err) + } + err = encoding.EncodeVarint(w, node.size) + if err != nil { + return fmt.Errorf("writing size, %w", err) + } + + // Unlike writeHashBytes, key is written for inner nodes. + err = encoding.EncodeBytes(w, node.key) + if err != nil { + return fmt.Errorf("writing key, %w", err) + } + + if node.isLeaf() { + err = encoding.EncodeBytes(w, node.value) + if err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + err = encoding.EncodeBytes(w, node.hash) + if err != nil { + return fmt.Errorf("writing hash, %w", err) + } + mode := 0 + if node.leftNodeKey == nil { + return ErrLeftNodeKeyEmpty + } + // check if children NodeKeys are legacy mode + if len(node.leftNodeKey) == hashSize { + mode += ModeLegacyLeftNode + } + if len(node.rightNodeKey) == hashSize { + mode += ModeLegacyRightNode + } + err = encoding.EncodeVarint(w, int64(mode)) + if err != nil { + return fmt.Errorf("writing mode, %w", err) + } + if mode&ModeLegacyLeftNode != 0 { // legacy leftNodeKey + err = encoding.EncodeBytes(w, node.leftNodeKey) + if err != nil { + return fmt.Errorf("writing the legacy left node key, %w", err) + } + } else { + leftNodeKey := GetNodeKey(node.leftNodeKey) + err = encoding.EncodeVarint(w, leftNodeKey.version) + if err != nil { + return fmt.Errorf("writing the version of left node key, %w", err) + } + err = encoding.EncodeVarint(w, int64(leftNodeKey.nonce)) + if err != nil { + return fmt.Errorf("writing the nonce of left node key, %w", err) + } + } + if node.rightNodeKey == nil { + return ErrRightNodeKeyEmpty + } + if mode&ModeLegacyRightNode != 0 { // legacy rightNodeKey + err = encoding.EncodeBytes(w, node.rightNodeKey) + if err != nil { + return fmt.Errorf("writing the legacy right node key, %w", err) + } + } else { + rightNodeKey := GetNodeKey(node.rightNodeKey) + err = encoding.EncodeVarint(w, rightNodeKey.version) + if err != nil { + return fmt.Errorf("writing the version of right node key, %w", err) + } + err = encoding.EncodeVarint(w, int64(rightNodeKey.nonce)) + if err != nil { + return fmt.Errorf("writing the nonce of right node key, %w", err) + } + } + } + return nil +} + +func (node *Node) getLeftNode(t *ImmutableTree) (*Node, error) { + if node.leftNode != nil { + return node.leftNode, nil + } + leftNode, err := t.ndb.GetNode(node.leftNodeKey) + if err != nil { + return nil, err + } + return leftNode, nil +} + +func (node *Node) getRightNode(t *ImmutableTree) (*Node, error) { + if node.rightNode != nil { + return node.rightNode, nil + } + rightNode, err := t.ndb.GetNode(node.rightNodeKey) + if err != nil { + return nil, err + } + return rightNode, nil +} + +// NOTE: mutates height and size +func (node *Node) calcHeightAndSize(t *ImmutableTree) error { + leftNode, err := node.getLeftNode(t) + if err != nil { + return err + } + + rightNode, err := node.getRightNode(t) + if err != nil { + return err + } + + node.subtreeHeight = maxInt8(leftNode.subtreeHeight, rightNode.subtreeHeight) + 1 + node.size = leftNode.size + rightNode.size + return nil +} + +func (node *Node) calcBalance(t *ImmutableTree) (int, error) { + leftNode, err := node.getLeftNode(t) + if err != nil { + return 0, err + } + + rightNode, err := node.getRightNode(t) + if err != nil { + return 0, err + } + + return int(leftNode.subtreeHeight) - int(rightNode.subtreeHeight), nil +} + +// traverse is a wrapper over traverseInRange when we want the whole tree +func (node *Node) traverse(t *ImmutableTree, ascending bool, cb func(*Node) bool) bool { + return node.traverseInRange(t, nil, nil, ascending, false, false, func(node *Node) bool { + return cb(node) + }) +} + +// traversePost is a wrapper over traverseInRange when we want the whole tree post-order +func (node *Node) traversePost(t *ImmutableTree, ascending bool, cb func(*Node) bool) bool { + return node.traverseInRange(t, nil, nil, ascending, false, true, func(node *Node) bool { + return cb(node) + }) +} + +func (node *Node) traverseInRange(tree *ImmutableTree, start, end []byte, ascending bool, inclusive bool, post bool, cb func(*Node) bool) bool { + stop := false + t := node.newTraversal(tree, start, end, ascending, inclusive, post) + // TODO: figure out how to handle these errors + for node2, err := t.next(); node2 != nil && err == nil; node2, err = t.next() { + stop = cb(node2) + if stop { + return stop + } + } + return stop +} + +var ( + ErrCloneLeafNode = fmt.Errorf("attempt to copy a leaf node") + ErrEmptyChild = fmt.Errorf("found an empty child") + ErrLeftNodeKeyEmpty = fmt.Errorf("node.leftNodeKey was empty in writeBytes") + ErrRightNodeKeyEmpty = fmt.Errorf("node.rightNodeKey was empty in writeBytes") + ErrLeftHashIsNil = fmt.Errorf("node.leftHash was nil in writeBytes") + ErrRightHashIsNil = fmt.Errorf("node.rightHash was nil in writeBytes") +) diff --git a/iavl/node_test.go b/iavl/node_test.go new file mode 100644 index 000000000..61cb6e638 --- /dev/null +++ b/iavl/node_test.go @@ -0,0 +1,264 @@ +package iavl + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +func TestNode_encodedSize(t *testing.T) { + nodeKey := &NodeKey{ + version: 1, + nonce: 1, + } + node := &Node{ + key: iavlrand.RandBytes(10), + value: iavlrand.RandBytes(10), + subtreeHeight: 0, + size: 100, + hash: iavlrand.RandBytes(20), + nodeKey: nodeKey, + leftNodeKey: nodeKey.GetKey(), + leftNode: nil, + rightNodeKey: nodeKey.GetKey(), + rightNode: nil, + } + + // leaf node + require.Equal(t, 25, node.encodedSize()) + + // non-leaf node + node.subtreeHeight = 1 + require.Equal(t, 39, node.encodedSize()) +} + +func TestNode_encode_decode(t *testing.T) { + childNodeKey := &NodeKey{ + version: 1, + nonce: 1, + } + childNodeHash := []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe} + testcases := map[string]struct { + node *Node + expectHex string + expectError bool + }{ + "nil": {nil, "", true}, + "inner": {&Node{ + subtreeHeight: 3, + size: 7, + key: []byte("key"), + nodeKey: &NodeKey{ + version: 2, + nonce: 1, + }, + leftNodeKey: childNodeKey.GetKey(), + rightNodeKey: childNodeKey.GetKey(), + hash: []byte{0x70, 0x80, 0x90, 0xa0}, + }, "060e036b657904708090a00002020202", false}, + "inner hybrid": {&Node{ + subtreeHeight: 3, + size: 7, + key: []byte("key"), + nodeKey: &NodeKey{ + version: 2, + nonce: 1, + }, + leftNodeKey: childNodeKey.GetKey(), + rightNodeKey: childNodeHash, + hash: []byte{0x70, 0x80, 0x90, 0xa0}, + }, "060e036b657904708090a0040202207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e", false}, + "leaf": {&Node{ + subtreeHeight: 0, + size: 1, + key: []byte("key"), + value: []byte("value"), + nodeKey: &NodeKey{ + version: 3, + nonce: 1, + }, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + }, "0002036b65790576616c7565", false}, + } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + var buf bytes.Buffer + err := tc.node.writeBytes(&buf) + if tc.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectHex, hex.EncodeToString(buf.Bytes())) + + node, err := MakeNode(tc.node.GetKey(), buf.Bytes()) + require.NoError(t, err) + // since key and value is always decoded to []byte{} we augment the expected struct here + if tc.node.key == nil { + tc.node.key = []byte{} + } + if tc.node.value == nil && tc.node.subtreeHeight == 0 { + tc.node.value = []byte{} + } + require.Equal(t, tc.node, node) + }) + } +} + +func TestNode_validate(t *testing.T) { + k := []byte("key") + v := []byte("value") + nk := &NodeKey{ + version: 1, + nonce: 1, + } + c := &Node{key: []byte("child"), value: []byte("x"), size: 1} + + testcases := map[string]struct { + node *Node + valid bool + }{ + "nil node": {nil, false}, + "leaf": {&Node{key: k, value: v, nodeKey: nk, size: 1}, true}, + "leaf with nil key": {&Node{key: nil, value: v, size: 1}, false}, + "leaf with empty key": {&Node{key: []byte{}, value: v, nodeKey: nk, size: 1}, true}, + "leaf with nil value": {&Node{key: k, value: nil, size: 1}, false}, + "leaf with empty value": {&Node{key: k, value: []byte{}, nodeKey: nk, size: 1}, true}, + "leaf with version 0": {&Node{key: k, value: v, size: 1}, false}, + "leaf with version -1": {&Node{key: k, value: v, size: 1}, false}, + "leaf with size 0": {&Node{key: k, value: v, size: 0}, false}, + "leaf with size 2": {&Node{key: k, value: v, size: 2}, false}, + "leaf with size -1": {&Node{key: k, value: v, size: -1}, false}, + "leaf with left node key": {&Node{key: k, value: v, size: 1, leftNodeKey: nk.GetKey()}, false}, + "leaf with left child": {&Node{key: k, value: v, size: 1, leftNode: c}, false}, + "leaf with right node key": {&Node{key: k, value: v, size: 1, rightNodeKey: nk.GetKey()}, false}, + "leaf with right child": {&Node{key: k, value: v, size: 1, rightNode: c}, false}, + "inner": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, leftNodeKey: nk.GetKey(), rightNodeKey: nk.GetKey()}, true}, + "inner with nil key": {&Node{key: nil, value: v, size: 1, subtreeHeight: 1, leftNodeKey: nk.GetKey(), rightNodeKey: nk.GetKey()}, false}, + "inner with value": {&Node{key: k, value: v, size: 1, subtreeHeight: 1, leftNodeKey: nk.GetKey(), rightNodeKey: nk.GetKey()}, false}, + "inner with empty value": {&Node{key: k, value: []byte{}, size: 1, subtreeHeight: 1, leftNodeKey: nk.GetKey(), rightNodeKey: nk.GetKey()}, false}, + "inner with left child": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, leftNodeKey: nk.GetKey()}, true}, + "inner with right child": {&Node{key: k, size: 1, subtreeHeight: 1, nodeKey: nk, rightNodeKey: nk.GetKey()}, true}, + "inner with no child": {&Node{key: k, size: 1, subtreeHeight: 1}, false}, + "inner with height 0": {&Node{key: k, size: 1, subtreeHeight: 0, leftNodeKey: nk.GetKey(), rightNodeKey: nk.GetKey()}, false}, + } + + for desc, tc := range testcases { + tc := tc // appease scopelint + t.Run(desc, func(t *testing.T) { + err := tc.node.validate() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func BenchmarkNode_encodedSize(b *testing.B) { + nk := &NodeKey{ + version: rand.Int63n(10000000), + nonce: uint32(rand.Int31n(10000000)), + } + node := &Node{ + key: iavlrand.RandBytes(25), + value: iavlrand.RandBytes(100), + nodeKey: nk, + subtreeHeight: 1, + size: rand.Int63n(10000000), + leftNodeKey: nk.GetKey(), + rightNodeKey: nk.GetKey(), + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + node.encodedSize() + } +} + +func BenchmarkNode_WriteBytes(b *testing.B) { + nk := &NodeKey{ + version: rand.Int63n(10000000), + nonce: uint32(rand.Int31n(10000000)), + } + node := &Node{ + key: iavlrand.RandBytes(25), + value: iavlrand.RandBytes(100), + nodeKey: nk, + subtreeHeight: 1, + size: rand.Int63n(10000000), + leftNodeKey: nk.GetKey(), + rightNodeKey: nk.GetKey(), + } + b.ResetTimer() + b.Run("NoPreAllocate", func(sub *testing.B) { + sub.ReportAllocs() + for i := 0; i < sub.N; i++ { + var buf bytes.Buffer + _ = node.writeBytes(&buf) + } + }) + b.Run("PreAllocate", func(sub *testing.B) { + sub.ReportAllocs() + for i := 0; i < sub.N; i++ { + var buf bytes.Buffer + buf.Grow(node.encodedSize()) + _ = node.writeBytes(&buf) + } + }) +} + +func BenchmarkNode_HashNode(b *testing.B) { + node := &Node{ + key: iavlrand.RandBytes(25), + value: iavlrand.RandBytes(100), + nodeKey: &NodeKey{ + version: rand.Int63n(10000000), + nonce: uint32(rand.Int31n(10000000)), + }, + subtreeHeight: 0, + size: rand.Int63n(10000000), + hash: iavlrand.RandBytes(32), + } + b.ResetTimer() + b.Run("NoBuffer", func(sub *testing.B) { + sub.ReportAllocs() + for i := 0; i < sub.N; i++ { + h := sha256.New() + require.NoError(b, node.writeHashBytes(h, node.nodeKey.version)) + _ = h.Sum(nil) + } + }) + b.Run("PreAllocate", func(sub *testing.B) { + sub.ReportAllocs() + for i := 0; i < sub.N; i++ { + h := sha256.New() + buf := new(bytes.Buffer) + buf.Grow(node.encodedSize()) + require.NoError(b, node.writeHashBytes(buf, node.nodeKey.version)) + _, err := h.Write(buf.Bytes()) + require.NoError(b, err) + _ = h.Sum(nil) + } + }) + b.Run("NoPreAllocate", func(sub *testing.B) { + sub.ReportAllocs() + for i := 0; i < sub.N; i++ { + h := sha256.New() + buf := new(bytes.Buffer) + require.NoError(b, node.writeHashBytes(buf, node.nodeKey.version)) + _, err := h.Write(buf.Bytes()) + require.NoError(b, err) + _ = h.Sum(nil) + } + }) +} diff --git a/iavl/nodedb.go b/iavl/nodedb.go new file mode 100644 index 000000000..b00aa6fac --- /dev/null +++ b/iavl/nodedb.go @@ -0,0 +1,1226 @@ +package iavl + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "math" + "sort" + "strconv" + "strings" + "sync" + "time" + + "cosmossdk.io/log" + + "github.com/cosmos/iavl/cache" + dbm "github.com/cosmos/iavl/db" + "github.com/cosmos/iavl/fastnode" + ibytes "github.com/cosmos/iavl/internal/bytes" + "github.com/cosmos/iavl/keyformat" +) + +const ( + int32Size = 4 + int64Size = 8 + hashSize = sha256.Size + genesisVersion = 1 + storageVersionKey = "storage_version" + // We store latest saved version together with storage version delimited by the constant below. + // This delimiter is valid only if fast storage is enabled (i.e. storageVersion >= fastStorageVersionValue). + // The latest saved version is needed for protection against downgrade and re-upgrade. In such a case, it would + // be possible to observe mismatch between the latest version state and the fast nodes on disk. + // Therefore, we would like to detect that and overwrite fast nodes on disk with the latest version state. + fastStorageVersionDelimiter = "-" + // Using semantic versioning: https://semver.org/ + defaultStorageVersionValue = "1.0.0" + fastStorageVersionValue = "1.1.0" + fastNodeCacheSize = 100000 + + // This is used to avoid the case which pruning blocks the main process. + deleteBatchCount = 1000 + deletePauseDuration = 100 * time.Millisecond +) + +var ( + // All new node keys are prefixed with the byte 's'. This ensures no collision is + // possible with the legacy nodes, and makes them easier to traverse. They are indexed by the version and the local nonce. + nodeKeyFormat = keyformat.NewFastPrefixFormatter('s', int64Size+int32Size) // s + + // This is only used for the iteration purpose. + nodeKeyPrefixFormat = keyformat.NewFastPrefixFormatter('s', int64Size) // s + + // Key Format for making reads and iterates go through a data-locality preserving db. + // The value at an entry will list what version it was written to. + // Then to query values, you first query state via this fast method. + // If its present, then check the tree version. If tree version >= result_version, + // return result_version. Else, go through old (slow) IAVL get method that walks through tree. + fastKeyFormat = keyformat.NewKeyFormat('f', 0) // f + + // Key Format for storing metadata about the chain such as the version number. + // The value at an entry will be in a variable format and up to the caller to + // decide how to parse. + metadataKeyFormat = keyformat.NewKeyFormat('m', 0) // m + + // All legacy node keys are prefixed with the byte 'n'. + legacyNodeKeyFormat = keyformat.NewFastPrefixFormatter('n', hashSize) // n + + // All legacy orphan keys are prefixed with the byte 'o'. + legacyOrphanKeyFormat = keyformat.NewKeyFormat('o', int64Size, int64Size, hashSize) // o + + // All legacy root keys are prefixed with the byte 'r'. + legacyRootKeyFormat = keyformat.NewKeyFormat('r', int64Size) // r + +) + +var errInvalidFastStorageVersion = fmt.Errorf("fast storage version must be in the format %s", fastStorageVersionDelimiter) + +type nodeDB struct { + logger log.Logger + + mtx sync.Mutex // Read/write lock. + db dbm.DB // Persistent node storage. + batch dbm.Batch // Batched writing buffer. + opts Options // Options to customize for pruning/writing + versionReaders map[int64]uint32 // Number of active version readers + storageVersion string // Storage version + firstVersion int64 // First version of nodeDB. + latestVersion int64 // Latest version of nodeDB. + legacyLatestVersion int64 // Latest version of nodeDB in legacy format. + nodeCache cache.Cache // Cache for nodes in the regular tree that consists of key-value pairs at any version. + fastNodeCache cache.Cache // Cache for nodes in the fast index that represents only key-value pairs at the latest version. +} + +func newNodeDB(db dbm.DB, cacheSize int, opts Options, lg log.Logger) *nodeDB { + storeVersion, err := db.Get(metadataKeyFormat.Key([]byte(storageVersionKey))) + + if err != nil || storeVersion == nil { + storeVersion = []byte(defaultStorageVersionValue) + } + + return &nodeDB{ + logger: lg, + db: db, + batch: NewBatchWithFlusher(db, opts.FlushThreshold), + opts: opts, + firstVersion: 0, + latestVersion: 0, // initially invalid + legacyLatestVersion: 0, + nodeCache: cache.New(cacheSize), + fastNodeCache: cache.New(fastNodeCacheSize), + versionReaders: make(map[int64]uint32, 8), + storageVersion: string(storeVersion), + } +} + +// GetNode gets a node from memory or disk. If it is an inner node, it does not +// load its children. +// It is used for both formats of nodes: legacy and new. +// `legacy`: nk is the hash of the node. `new`: . +func (ndb *nodeDB) GetNode(nk []byte) (*Node, error) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + if nk == nil { + return nil, ErrNodeMissingNodeKey + } + + // Check the cache. + if cachedNode := ndb.nodeCache.Get(nk); cachedNode != nil { + ndb.opts.Stat.IncCacheHitCnt() + return cachedNode.(*Node), nil + } + + ndb.opts.Stat.IncCacheMissCnt() + + // Doesn't exist, load. + isLegcyNode := len(nk) == hashSize + var nodeKey []byte + if isLegcyNode { + nodeKey = ndb.legacyNodeKey(nk) + } else { + nodeKey = ndb.nodeKey(nk) + } + buf, err := ndb.db.Get(nodeKey) + if err != nil { + return nil, fmt.Errorf("can't get node %v: %v", nk, err) + } + if buf == nil { + return nil, fmt.Errorf("Value missing for key %v corresponding to nodeKey %x", nk, nodeKey) + } + + var node *Node + if isLegcyNode { + node, err = MakeLegacyNode(nk, buf) + if err != nil { + return nil, fmt.Errorf("error reading Legacy Node. bytes: %x, error: %v", buf, err) + } + } else { + node, err = MakeNode(nk, buf) + if err != nil { + return nil, fmt.Errorf("error reading Node. bytes: %x, error: %v", buf, err) + } + } + + ndb.nodeCache.Add(node) + + return node, nil +} + +func (ndb *nodeDB) GetFastNode(key []byte) (*fastnode.Node, error) { + if !ndb.hasUpgradedToFastStorage() { + return nil, errors.New("storage version is not fast") + } + + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + if len(key) == 0 { + return nil, fmt.Errorf("nodeDB.GetFastNode() requires key, len(key) equals 0") + } + + if cachedFastNode := ndb.fastNodeCache.Get(key); cachedFastNode != nil { + ndb.opts.Stat.IncFastCacheHitCnt() + return cachedFastNode.(*fastnode.Node), nil + } + + ndb.opts.Stat.IncFastCacheMissCnt() + + // Doesn't exist, load. + buf, err := ndb.db.Get(ndb.fastNodeKey(key)) + if err != nil { + return nil, fmt.Errorf("can't get FastNode %X: %w", key, err) + } + if buf == nil { + return nil, nil + } + + fastNode, err := fastnode.DeserializeNode(key, buf) + if err != nil { + return nil, fmt.Errorf("error reading FastNode. bytes: %x, error: %w", buf, err) + } + ndb.fastNodeCache.Add(fastNode) + return fastNode, nil +} + +// SaveNode saves a node to disk. +func (ndb *nodeDB) SaveNode(node *Node) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + if node.nodeKey == nil { + return ErrNodeMissingNodeKey + } + + // Save node bytes to db. + var buf bytes.Buffer + buf.Grow(node.encodedSize()) + + if err := node.writeBytes(&buf); err != nil { + return err + } + + if err := ndb.batch.Set(ndb.nodeKey(node.GetKey()), buf.Bytes()); err != nil { + return err + } + + ndb.logger.Debug("BATCH SAVE", "node", node) + ndb.nodeCache.Add(node) + return nil +} + +// SaveFastNode saves a FastNode to disk and add to cache. +func (ndb *nodeDB) SaveFastNode(node *fastnode.Node) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + return ndb.saveFastNodeUnlocked(node, true) +} + +// SaveFastNodeNoCache saves a FastNode to disk without adding to cache. +func (ndb *nodeDB) SaveFastNodeNoCache(node *fastnode.Node) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + return ndb.saveFastNodeUnlocked(node, false) +} + +// SetFastStorageVersionToBatch sets storage version to fast where the version is +// 1.1.0-. Returns error if storage version is incorrect or on +// db error, nil otherwise. Requires changes to be committed after to be persisted. +func (ndb *nodeDB) SetFastStorageVersionToBatch(latestVersion int64) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + var newVersion string + if ndb.storageVersion >= fastStorageVersionValue { + // Storage version should be at index 0 and latest fast cache version at index 1 + versions := strings.Split(ndb.storageVersion, fastStorageVersionDelimiter) + + if len(versions) > 2 { + return errInvalidFastStorageVersion + } + + newVersion = versions[0] + } else { + newVersion = fastStorageVersionValue + } + + newVersion += fastStorageVersionDelimiter + strconv.Itoa(int(latestVersion)) + + if err := ndb.batch.Set(metadataKeyFormat.Key([]byte(storageVersionKey)), []byte(newVersion)); err != nil { + return err + } + ndb.storageVersion = newVersion + return nil +} + +func (ndb *nodeDB) getStorageVersion() string { + return ndb.storageVersion +} + +// Returns true if the upgrade to latest storage version has been performed, false otherwise. +func (ndb *nodeDB) hasUpgradedToFastStorage() bool { + return ndb.getStorageVersion() >= fastStorageVersionValue +} + +// Returns true if the upgrade to fast storage has occurred but it does not match the live state, false otherwise. +// When the live state is not matched, we must force reupgrade. +// We determine this by checking the version of the live state and the version of the live state when +// latest storage was updated on disk the last time. +func (ndb *nodeDB) shouldForceFastStorageUpgrade() (bool, error) { + versions := strings.Split(ndb.storageVersion, fastStorageVersionDelimiter) + + if len(versions) == 2 { + latestVersion, err := ndb.getLatestVersion() + if err != nil { + // TODO: should be true or false as default? (removed panic here) + return false, err + } + if versions[1] != strconv.Itoa(int(latestVersion)) { + return true, nil + } + } + return false, nil +} + +// saveFastNodeUnlocked saves a FastNode to disk. +func (ndb *nodeDB) saveFastNodeUnlocked(node *fastnode.Node, shouldAddToCache bool) error { + if node.GetKey() == nil { + return fmt.Errorf("cannot have FastNode with a nil value for key") + } + + // Save node bytes to db. + var buf bytes.Buffer + buf.Grow(node.EncodedSize()) + + if err := node.WriteBytes(&buf); err != nil { + return fmt.Errorf("error while writing fastnode bytes. Err: %w", err) + } + + if err := ndb.batch.Set(ndb.fastNodeKey(node.GetKey()), buf.Bytes()); err != nil { + return fmt.Errorf("error while writing key/val to nodedb batch. Err: %w", err) + } + if shouldAddToCache { + ndb.fastNodeCache.Add(node) + } + return nil +} + +// Has checks if a node key exists in the database. +func (ndb *nodeDB) Has(nk []byte) (bool, error) { + return ndb.db.Has(ndb.nodeKey(nk)) +} + +// deleteVersion deletes a tree version from disk. +// deletes orphans +func (ndb *nodeDB) deleteVersion(version int64) error { + rootKey, err := ndb.GetRoot(version) + if err != nil { + return err + } + + if err := ndb.traverseOrphans(version, version+1, func(orphan *Node) error { + if orphan.nodeKey.nonce == 0 && !orphan.isLegacy { + // if the orphan is a reformatted root, it can be a legacy root + // so it should be removed from the pruning process. + if err := ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)); err != nil { + return err + } + } + if orphan.nodeKey.nonce == 1 && orphan.nodeKey.version < version { + // if the orphan is referred to the previous root, it should be reformatted + // to (version, 0), because the root (version, 1) should be removed but not + // applied now due to the batch writing. + orphan.nodeKey.nonce = 0 + } + nk := orphan.GetKey() + if orphan.isLegacy { + return ndb.batch.Delete(ndb.legacyNodeKey(nk)) + } + return ndb.batch.Delete(ndb.nodeKey(nk)) + }); err != nil { + return err + } + + literalRootKey := GetRootKey(version) + if rootKey == nil || !bytes.Equal(rootKey, literalRootKey) { + // if the root key is not matched with the literal root key, it means the given root + // is a reference root to the previous version. + if err := ndb.batch.Delete(ndb.nodeKey(literalRootKey)); err != nil { + return err + } + } + + // check if the version is referred by the next version + nextRootKey, err := ndb.GetRoot(version + 1) + if err != nil { + return err + } + if bytes.Equal(literalRootKey, nextRootKey) { + root, err := ndb.GetNode(nextRootKey) + if err != nil { + return err + } + // ensure that the given version is not included in the root search + if err := ndb.batch.Delete(ndb.nodeKey(literalRootKey)); err != nil { + return err + } + // instead, the root should be reformatted to (version, 0) + root.nodeKey.nonce = 0 + if err := ndb.SaveNode(root); err != nil { + return err + } + } + + return nil +} + +// deleteLegacyNodes deletes all legacy nodes with the given version from disk. +// NOTE: This is only used for DeleteVersionsFrom. +func (ndb *nodeDB) deleteLegacyNodes(version int64, nk []byte) error { + node, err := ndb.GetNode(nk) + if err != nil { + return err + } + if node.nodeKey.version < version { + // it will skip the whole subtree. + return nil + } + if node.leftNodeKey != nil { + if err := ndb.deleteLegacyNodes(version, node.leftNodeKey); err != nil { + return err + } + } + if node.rightNodeKey != nil { + if err := ndb.deleteLegacyNodes(version, node.rightNodeKey); err != nil { + return err + } + } + return ndb.batch.Delete(ndb.legacyNodeKey(nk)) +} + +// deleteLegacyVersions deletes all legacy versions from disk. +func (ndb *nodeDB) deleteLegacyVersions(legacyLatestVersion int64) error { + count := 0 + + checkDeletePause := func() { + count++ + if count%deleteBatchCount == 0 { + time.Sleep(deletePauseDuration) + count = 0 + } + } + + // Delete the last version for the legacyLastVersion + if err := ndb.traverseOrphans(legacyLatestVersion, legacyLatestVersion+1, func(orphan *Node) error { + checkDeletePause() + return ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)) + }); err != nil { + return err + } + + // Delete orphans for all legacy versions + if err := ndb.traversePrefix(legacyOrphanKeyFormat.Key(), func(key, value []byte) error { + checkDeletePause() + if err := ndb.batch.Delete(key); err != nil { + return err + } + var fromVersion, toVersion int64 + legacyOrphanKeyFormat.Scan(key, &toVersion, &fromVersion) + if (fromVersion <= legacyLatestVersion && toVersion < legacyLatestVersion) || fromVersion > legacyLatestVersion { + checkDeletePause() + return ndb.batch.Delete(ndb.legacyNodeKey(value)) + } + return nil + }); err != nil { + return err + } + // Delete all legacy roots + if err := ndb.traversePrefix(legacyRootKeyFormat.Key(), func(key, value []byte) error { + checkDeletePause() + return ndb.batch.Delete(key) + }); err != nil { + return err + } + + return nil +} + +// DeleteVersionsFrom permanently deletes all tree versions from the given version upwards. +func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { + latest, err := ndb.getLatestVersion() + if err != nil { + return err + } + if latest < fromVersion { + return nil + } + + ndb.mtx.Lock() + for v, r := range ndb.versionReaders { + if v >= fromVersion && r != 0 { + ndb.mtx.Unlock() // Unlock before exiting + return fmt.Errorf("unable to delete version %v with %v active readers", v, r) + } + } + ndb.mtx.Unlock() + + // Delete the legacy versions + legacyLatestVersion, err := ndb.getLegacyLatestVersion() + if err != nil { + return err + } + dumpFromVersion := fromVersion + if legacyLatestVersion >= fromVersion { + if err := ndb.traverseRange(legacyRootKeyFormat.Key(fromVersion), legacyRootKeyFormat.Key(legacyLatestVersion+1), func(k, v []byte) error { + var version int64 + legacyRootKeyFormat.Scan(k, &version) + // delete the legacy nodes + if err := ndb.deleteLegacyNodes(version, v); err != nil { + return err + } + // it will skip the orphans because orphans will be removed at once in `deleteLegacyVersions` + // delete the legacy root + return ndb.batch.Delete(k) + }); err != nil { + return err + } + // Update the legacy latest version forcibly + ndb.legacyLatestVersion = 0 + fromVersion = legacyLatestVersion + 1 + } + + // Delete the nodes for new format + err = ndb.traverseRange(nodeKeyPrefixFormat.KeyInt64(fromVersion), nodeKeyPrefixFormat.KeyInt64(latest+1), func(k, v []byte) error { + return ndb.batch.Delete(k) + }) + + if err != nil { + return err + } + + // NOTICE: we don't touch fast node indexes here, because it'll be rebuilt later because of version mismatch. + + ndb.resetLatestVersion(dumpFromVersion - 1) + + return nil +} + +// DeleteVersionsTo deletes the oldest versions up to the given version from disk. +func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { + legacyLatestVersion, err := ndb.getLegacyLatestVersion() + if err != nil { + return err + } + + // If the legacy version is greater than the toVersion, we don't need to delete anything. + // It will delete the legacy versions at once. + if legacyLatestVersion > toVersion { + return nil + } + + first, err := ndb.getFirstVersion() + if err != nil { + return err + } + + latest, err := ndb.getLatestVersion() + if err != nil { + return err + } + + if latest <= toVersion { + return fmt.Errorf("latest version %d is less than or equal to toVersion %d", latest, toVersion) + } + + ndb.mtx.Lock() + for v, r := range ndb.versionReaders { + if v >= first && v <= toVersion && r != 0 { + ndb.mtx.Unlock() + return fmt.Errorf("unable to delete version %d with %d active readers", v, r) + } + } + ndb.mtx.Unlock() + + // Delete the legacy versions + if legacyLatestVersion >= first { + // reset the legacy latest version forcibly to avoid multiple calls + ndb.resetLegacyLatestVersion(-1) + go func() { + if err := ndb.deleteLegacyVersions(legacyLatestVersion); err != nil { + ndb.logger.Error("Error deleting legacy versions", "err", err) + } + }() + first = legacyLatestVersion + 1 + } + + for version := first; version <= toVersion; version++ { + if err := ndb.deleteVersion(version); err != nil { + return err + } + ndb.resetFirstVersion(version + 1) + } + + return nil +} + +func (ndb *nodeDB) DeleteFastNode(key []byte) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + if err := ndb.batch.Delete(ndb.fastNodeKey(key)); err != nil { + return err + } + ndb.fastNodeCache.Remove(key) + return nil +} + +func (ndb *nodeDB) nodeKey(nk []byte) []byte { + return nodeKeyFormat.Key(nk) +} + +func (ndb *nodeDB) fastNodeKey(key []byte) []byte { + return fastKeyFormat.KeyBytes(key) +} + +func (ndb *nodeDB) legacyNodeKey(nk []byte) []byte { + return legacyNodeKeyFormat.Key(nk) +} + +func (ndb *nodeDB) legacyRootKey(version int64) []byte { + return legacyRootKeyFormat.Key(version) +} + +func (ndb *nodeDB) getFirstVersion() (int64, error) { + ndb.mtx.Lock() + firstVersion := ndb.firstVersion + ndb.mtx.Unlock() + + if firstVersion > 0 { + return firstVersion, nil + } + + // Check if we have a legacy version + itr, err := ndb.getPrefixIterator(legacyRootKeyFormat.Key()) + if err != nil { + return 0, err + } + defer itr.Close() + if itr.Valid() { + var version int64 + legacyRootKeyFormat.Scan(itr.Key(), &version) + return version, nil + } + // Find the first version + latestVersion, err := ndb.getLatestVersion() + if err != nil { + return 0, err + } + for firstVersion < latestVersion { + version := (latestVersion + firstVersion) >> 1 + has, err := ndb.hasVersion(version) + if err != nil { + return 0, err + } + if has { + latestVersion = version + } else { + firstVersion = version + 1 + } + } + + ndb.resetFirstVersion(latestVersion) + + return latestVersion, nil +} + +func (ndb *nodeDB) resetFirstVersion(version int64) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + ndb.firstVersion = version +} + +func (ndb *nodeDB) getLegacyLatestVersion() (int64, error) { + ndb.mtx.Lock() + latestVersion := ndb.legacyLatestVersion + ndb.mtx.Unlock() + + if latestVersion != 0 { + return latestVersion, nil + } + + itr, err := ndb.db.ReverseIterator( + legacyRootKeyFormat.Key(int64(1)), + legacyRootKeyFormat.Key(int64(math.MaxInt64)), + ) + if err != nil { + return 0, err + } + defer itr.Close() + + if itr.Valid() { + k := itr.Key() + var version int64 + legacyRootKeyFormat.Scan(k, &version) + ndb.resetLegacyLatestVersion(version) + return version, nil + } + + if err := itr.Error(); err != nil { + return 0, err + } + + // If there are no legacy versions, set -1 + ndb.resetLegacyLatestVersion(-1) + + return -1, nil +} + +func (ndb *nodeDB) resetLegacyLatestVersion(version int64) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + ndb.legacyLatestVersion = version +} + +func (ndb *nodeDB) getLatestVersion() (int64, error) { + ndb.mtx.Lock() + latestVersion := ndb.latestVersion + ndb.mtx.Unlock() + + if latestVersion > 0 { + return latestVersion, nil + } + + itr, err := ndb.db.ReverseIterator( + nodeKeyPrefixFormat.KeyInt64(int64(1)), + nodeKeyPrefixFormat.KeyInt64(int64(math.MaxInt64)), + ) + if err != nil { + return 0, err + } + defer itr.Close() + + if itr.Valid() { + k := itr.Key() + var nk []byte + nodeKeyFormat.Scan(k, &nk) + latestVersion = GetNodeKey(nk).version + ndb.resetLatestVersion(latestVersion) + return latestVersion, nil + } + + if err := itr.Error(); err != nil { + return 0, err + } + + // If there are no versions, try to get the latest version from the legacy format. + latestVersion, err = ndb.getLegacyLatestVersion() + if err != nil { + return 0, err + } + if latestVersion > 0 { + ndb.resetLatestVersion(latestVersion) + return latestVersion, nil + } + + return 0, nil +} + +func (ndb *nodeDB) resetLatestVersion(version int64) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + ndb.latestVersion = version +} + +// hasVersion checks if the given version exists. +func (ndb *nodeDB) hasVersion(version int64) (bool, error) { + return ndb.db.Has(nodeKeyFormat.Key(GetRootKey(version))) +} + +// hasLegacyVersion checks if the given version exists in the legacy format. +func (ndb *nodeDB) hasLegacyVersion(version int64) (bool, error) { + return ndb.db.Has(ndb.legacyRootKey(version)) +} + +// GetRoot gets the nodeKey of the root for the specific version. +func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { + rootKey := GetRootKey(version) + val, err := ndb.db.Get(nodeKeyFormat.Key(rootKey)) + if err != nil { + return nil, err + } + if val == nil { + // try the legacy root key + val, err := ndb.db.Get(ndb.legacyRootKey(version)) + if err != nil { + return nil, err + } + if val == nil { + return nil, ErrVersionDoesNotExist + } + if len(val) == 0 { // empty root + return nil, nil + } + return val, nil + } + if len(val) == 0 { // empty root + return nil, nil + } + isRef, n := isReferenceRoot(val) + if isRef { // point to the prev version + switch n { + case nodeKeyFormat.Length(): // (prefix, version, 1) + nk := GetNodeKey(val[1:]) + val, err = ndb.db.Get(nodeKeyFormat.Key(val[1:])) + if err != nil { + return nil, err + } + if val == nil { // the prev version does not exist + // check if the prev version root is reformatted due to the pruning + rnk := &NodeKey{version: nk.version, nonce: 0} + val, err = ndb.db.Get(nodeKeyFormat.Key(rnk.GetKey())) + if err != nil { + return nil, err + } + if val == nil { + return nil, ErrVersionDoesNotExist + } + return rnk.GetKey(), nil + } + return nk.GetKey(), nil + case nodeKeyPrefixFormat.Length(): // (prefix, version) before the lazy pruning + return append(val[1:], 0, 0, 0, 1), nil + default: + return nil, fmt.Errorf("invalid reference root: %x", val) + } + } + + return rootKey, nil +} + +// SaveEmptyRoot saves the empty root. +func (ndb *nodeDB) SaveEmptyRoot(version int64) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + return ndb.batch.Set(nodeKeyFormat.Key(GetRootKey(version)), []byte{}) +} + +// SaveRoot saves the root when no updates. +func (ndb *nodeDB) SaveRoot(version int64, nk *NodeKey) error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + return ndb.batch.Set(nodeKeyFormat.Key(GetRootKey(version)), nodeKeyFormat.Key(nk.GetKey())) +} + +// Traverse fast nodes and return error if any, nil otherwise +func (ndb *nodeDB) traverseFastNodes(fn func(k, v []byte) error) error { + return ndb.traversePrefix(fastKeyFormat.Key(), fn) +} + +// Traverse all keys and return error if any, nil otherwise + +func (ndb *nodeDB) traverse(fn func(key, value []byte) error) error { + return ndb.traverseRange(nil, nil, fn) +} + +// Traverse all keys between a given range (excluding end) and return error if any, nil otherwise +func (ndb *nodeDB) traverseRange(start []byte, end []byte, fn func(k, v []byte) error) error { + itr, err := ndb.db.Iterator(start, end) + if err != nil { + return err + } + defer itr.Close() + + for ; itr.Valid(); itr.Next() { + if err := fn(itr.Key(), itr.Value()); err != nil { + return err + } + } + + return itr.Error() +} + +// Traverse all keys with a certain prefix. Return error if any, nil otherwise +func (ndb *nodeDB) traversePrefix(prefix []byte, fn func(k, v []byte) error) error { + itr, err := ndb.getPrefixIterator(prefix) + if err != nil { + return err + } + defer itr.Close() + + for ; itr.Valid(); itr.Next() { + if err := fn(itr.Key(), itr.Value()); err != nil { + return err + } + } + + return nil +} + +// Get the iterator for a given prefix. +func (ndb *nodeDB) getPrefixIterator(prefix []byte) (dbm.Iterator, error) { + var start, end []byte + if len(prefix) == 0 { + start = nil + end = nil + } else { + start = ibytes.Cp(prefix) + end = ibytes.CpIncr(prefix) + } + + return ndb.db.Iterator(start, end) +} + +// Get iterator for fast prefix and error, if any +func (ndb *nodeDB) getFastIterator(start, end []byte, ascending bool) (dbm.Iterator, error) { + var startFormatted, endFormatted []byte + + if start != nil { + startFormatted = fastKeyFormat.KeyBytes(start) + } else { + startFormatted = fastKeyFormat.Key() + } + + if end != nil { + endFormatted = fastKeyFormat.KeyBytes(end) + } else { + endFormatted = fastKeyFormat.Key() + endFormatted[0]++ + } + + if ascending { + return ndb.db.Iterator(startFormatted, endFormatted) + } + + return ndb.db.ReverseIterator(startFormatted, endFormatted) +} + +// Write to disk. +func (ndb *nodeDB) Commit() error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + var err error + if ndb.opts.Sync { + err = ndb.batch.WriteSync() + } else { + err = ndb.batch.Write() + } + if err != nil { + return fmt.Errorf("failed to write batch, %w", err) + } + + return nil +} + +func (ndb *nodeDB) incrVersionReaders(version int64) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + ndb.versionReaders[version]++ +} + +func (ndb *nodeDB) decrVersionReaders(version int64) { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + if ndb.versionReaders[version] > 0 { + ndb.versionReaders[version]-- + } + if ndb.versionReaders[version] == 0 { + delete(ndb.versionReaders, version) + } +} + +func isReferenceRoot(bz []byte) (bool, int) { + if bz[0] == nodeKeyFormat.Prefix()[0] { + return true, len(bz) + } + return false, 0 +} + +// traverseOrphans traverses orphans which removed by the updates of the curVersion in the prevVersion. +// NOTE: it is used for both legacy and new nodes. +func (ndb *nodeDB) traverseOrphans(prevVersion, curVersion int64, fn func(*Node) error) error { + curKey, err := ndb.GetRoot(curVersion) + if err != nil { + return err + } + + curIter, err := NewNodeIterator(curKey, ndb) + if err != nil { + return err + } + + prevKey, err := ndb.GetRoot(prevVersion) + if err != nil { + return err + } + prevIter, err := NewNodeIterator(prevKey, ndb) + if err != nil { + return err + } + + var orgNode *Node + for prevIter.Valid() { + for orgNode == nil && curIter.Valid() { + node := curIter.GetNode() + if node.nodeKey.version <= prevVersion { + curIter.Next(true) + orgNode = node + } else { + curIter.Next(false) + } + } + pNode := prevIter.GetNode() + + if orgNode != nil && bytes.Equal(pNode.hash, orgNode.hash) { + prevIter.Next(true) + orgNode = nil + } else { + err = fn(pNode) + if err != nil { + return err + } + prevIter.Next(false) + } + } + + return nil +} + +// Close the nodeDB. +func (ndb *nodeDB) Close() error { + ndb.mtx.Lock() + defer ndb.mtx.Unlock() + + if ndb.batch != nil { + if err := ndb.batch.Close(); err != nil { + return err + } + ndb.batch = nil + } + + // skip the db.Close() since it can be used by other trees + return nil +} + +// Utility and test functions + +func (ndb *nodeDB) leafNodes() ([]*Node, error) { + leaves := []*Node{} + + err := ndb.traverseNodes(func(node *Node) error { + if node.isLeaf() { + leaves = append(leaves, node) + } + return nil + }) + if err != nil { + return nil, err + } + + return leaves, nil +} + +func (ndb *nodeDB) nodes() ([]*Node, error) { + nodes := []*Node{} + + err := ndb.traverseNodes(func(node *Node) error { + nodes = append(nodes, node) + return nil + }) + if err != nil { + return nil, err + } + + return nodes, nil +} + +func (ndb *nodeDB) legacyNodes() ([]*Node, error) { + nodes := []*Node{} + + err := ndb.traversePrefix(legacyNodeKeyFormat.Prefix(), func(key, value []byte) error { + node, err := MakeLegacyNode(key[1:], value) + if err != nil { + return err + } + nodes = append(nodes, node) + return nil + }) + if err != nil { + return nil, err + } + + return nodes, nil +} + +func (ndb *nodeDB) orphans() ([][]byte, error) { + orphans := [][]byte{} + + for version := ndb.firstVersion; version < ndb.latestVersion; version++ { + err := ndb.traverseOrphans(version, version+1, func(orphan *Node) error { + orphans = append(orphans, orphan.hash) + return nil + }) + if err != nil { + return nil, err + } + } + + return orphans, nil +} + +// Not efficient. +// NOTE: DB cannot implement Size() because +// mutations are not always synchronous. +// + +func (ndb *nodeDB) size() int { + size := 0 + err := ndb.traverse(func(k, v []byte) error { + size++ + return nil + }) + if err != nil { + return -1 + } + return size +} + +func (ndb *nodeDB) traverseNodes(fn func(node *Node) error) error { + nodes := []*Node{} + + if err := ndb.traversePrefix(nodeKeyFormat.Prefix(), func(key, value []byte) error { + if isRef, _ := isReferenceRoot(value); isRef { + return nil + } + node, err := MakeNode(key[1:], value) + if err != nil { + return err + } + nodes = append(nodes, node) + return nil + }); err != nil { + return err + } + + sort.Slice(nodes, func(i, j int) bool { + return bytes.Compare(nodes[i].key, nodes[j].key) < 0 + }) + + for _, n := range nodes { + if err := fn(n); err != nil { + return err + } + } + return nil +} + +// traverseStateChanges iterate the range of versions, compare each version to it's predecessor to extract the state changes of it. +// endVersion is exclusive, set to `math.MaxInt64` to cover the latest version. +func (ndb *nodeDB) traverseStateChanges(startVersion, endVersion int64, fn func(version int64, changeSet *ChangeSet) error) error { + firstVersion, err := ndb.getFirstVersion() + if err != nil { + return err + } + if startVersion < firstVersion { + startVersion = firstVersion + } + latestVersion, err := ndb.getLatestVersion() + if err != nil { + return err + } + if endVersion > latestVersion { + endVersion = latestVersion + } + + prevVersion := startVersion - 1 + prevRoot, err := ndb.GetRoot(prevVersion) + if err != nil && err != ErrVersionDoesNotExist { + return err + } + + for version := startVersion; version <= endVersion; version++ { + root, err := ndb.GetRoot(version) + if err != nil { + return err + } + + var changeSet ChangeSet + receiveKVPair := func(pair *KVPair) error { + changeSet.Pairs = append(changeSet.Pairs, pair) + return nil + } + + if err := ndb.extractStateChanges(prevVersion, prevRoot, root, receiveKVPair); err != nil { + return err + } + + if err := fn(version, &changeSet); err != nil { + return err + } + prevVersion = version + prevRoot = root + } + + return nil +} + +func (ndb *nodeDB) String() (string, error) { + buf := bufPool.Get().(*bytes.Buffer) + defer bufPool.Put(buf) + buf.Reset() + + index := 0 + + err := ndb.traversePrefix(nodeKeyFormat.Prefix(), func(key, value []byte) error { + fmt.Fprintf(buf, "%s: %x\n", key, value) + return nil + }) + if err != nil { + return "", err + } + + buf.WriteByte('\n') + + err = ndb.traverseNodes(func(node *Node) error { + switch { + case node == nil: + fmt.Fprintf(buf, "%s: \n", nodeKeyFormat.Prefix()) + case node.value == nil && node.subtreeHeight > 0: + fmt.Fprintf(buf, "%s: %s %-16s h=%d nodeKey=%v\n", + nodeKeyFormat.Prefix(), node.key, "", node.subtreeHeight, node.nodeKey) + default: + fmt.Fprintf(buf, "%s: %s = %-16s h=%d nodeKey=%v\n", + nodeKeyFormat.Prefix(), node.key, node.value, node.subtreeHeight, node.nodeKey) + } + index++ + return nil + }) + + if err != nil { + return "", err + } + + return "-" + "\n" + buf.String() + "-", nil +} + +var ErrNodeMissingNodeKey = fmt.Errorf("node does not have a nodeKey") diff --git a/iavl/nodedb_test.go b/iavl/nodedb_test.go new file mode 100644 index 000000000..52d6a51de --- /dev/null +++ b/iavl/nodedb_test.go @@ -0,0 +1,439 @@ +package iavl + +import ( + "errors" + "fmt" + "strconv" + "testing" + "time" + + log "cosmossdk.io/log" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + "github.com/cosmos/iavl/mock" +) + +func BenchmarkNodeKey(b *testing.B) { + ndb := &nodeDB{} + + for i := 0; i < b.N; i++ { + nk := &NodeKey{ + version: int64(i), + nonce: uint32(i), + } + ndb.nodeKey(nk.GetKey()) + } +} + +func BenchmarkTreeString(b *testing.B) { + tree := makeAndPopulateMutableTree(b) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink, _ = tree.String() + require.NotNil(b, sink) + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func TestNewNoDbStorage_StorageVersionInDb_Success(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(expectedVersion), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + + ndb := newNodeDB(dbMock, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.storageVersion) +} + +func TestNewNoDbStorage_ErrorInConstructor_DefaultSet(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, errors.New("some db error")).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + ndb := newNodeDB(dbMock, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.getStorageVersion()) +} + +func TestNewNoDbStorage_DoesNotExist_DefaultSet(t *testing.T) { + const expectedVersion = defaultStorageVersionValue + + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + + dbMock.EXPECT().Get(gomock.Any()).Return(nil, nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(nil).Times(1) + + ndb := newNodeDB(dbMock, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, expectedVersion, ndb.getStorageVersion()) +} + +func TestSetStorageVersion_Success(t *testing.T) { + const expectedVersion = fastStorageVersionValue + + db := dbm.NewMemDB() + + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + latestVersion, err := ndb.getLatestVersion() + require.NoError(t, err) + + err = ndb.SetFastStorageVersionToBatch(latestVersion) + require.NoError(t, err) + + require.Equal(t, expectedVersion+fastStorageVersionDelimiter+strconv.Itoa(int(latestVersion)), ndb.getStorageVersion()) + require.NoError(t, ndb.batch.Write()) +} + +func TestSetStorageVersion_DBFailure_OldKept(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + + expectedErrorMsg := "some db error" + + expectedFastCacheVersion := 2 + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(defaultStorageVersionValue), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + + batchMock.EXPECT().GetByteSize().Return(100, nil).Times(1) + batchMock.EXPECT().Set(metadataKeyFormat.Key([]byte(storageVersionKey)), []byte(fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(expectedFastCacheVersion))).Return(errors.New(expectedErrorMsg)).Times(1) + + ndb := newNodeDB(dbMock, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(int64(expectedFastCacheVersion)) + require.Error(t, err) + require.Equal(t, expectedErrorMsg, err.Error()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) +} + +func TestSetStorageVersion_InvalidVersionFailure_OldKept(t *testing.T) { + ctrl := gomock.NewController(t) + dbMock := mock.NewMockDB(ctrl) + batchMock := mock.NewMockBatch(ctrl) + + expectedErrorMsg := errInvalidFastStorageVersion + + invalidStorageVersion := fastStorageVersionValue + fastStorageVersionDelimiter + "1" + fastStorageVersionDelimiter + "2" + + dbMock.EXPECT().Get(gomock.Any()).Return([]byte(invalidStorageVersion), nil).Times(1) + dbMock.EXPECT().NewBatchWithSize(gomock.Any()).Return(batchMock).Times(1) + + ndb := newNodeDB(dbMock, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, invalidStorageVersion, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(0) + require.Error(t, err) + require.Equal(t, expectedErrorMsg, err) + require.Equal(t, invalidStorageVersion, ndb.getStorageVersion()) +} + +func TestSetStorageVersion_FastVersionFirst_VersionAppended(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.storageVersion = fastStorageVersionValue + ndb.latestVersion = 100 + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) +} + +func TestSetStorageVersion_FastVersionSecond_VersionAppended(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + + storageVersionBytes := []byte(fastStorageVersionValue) + storageVersionBytes[len(fastStorageVersionValue)-1]++ // increment last byte + ndb.storageVersion = string(storageVersionBytes) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, string(storageVersionBytes)+fastStorageVersionDelimiter+strconv.Itoa(int(ndb.latestVersion)), ndb.storageVersion) +} + +func TestSetStorageVersion_SameVersionTwice(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + + storageVersionBytes := []byte(fastStorageVersionValue) + storageVersionBytes[len(fastStorageVersionValue)-1]++ // increment last byte + ndb.storageVersion = string(storageVersionBytes) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + newStorageVersion := string(storageVersionBytes) + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + require.Equal(t, newStorageVersion, ndb.storageVersion) + + err = ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + require.Equal(t, newStorageVersion, ndb.storageVersion) +} + +// Test case where version is incorrect and has some extra garbage at the end +func TestShouldForceFastStorageUpdate_DefaultVersion_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.storageVersion = defaultStorageVersionValue + ndb.latestVersion = 100 + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + +func TestShouldForceFastStorageUpdate_FastVersion_Greater_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion+1)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.True(t, shouldForce) + require.NoError(t, err) +} + +func TestShouldForceFastStorageUpdate_FastVersion_Smaller_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion-1)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.True(t, shouldForce) + require.NoError(t, err) +} + +func TestShouldForceFastStorageUpdate_FastVersion_Match_False(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + +func TestIsFastStorageEnabled_True(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = fastStorageVersionValue + fastStorageVersionDelimiter + strconv.Itoa(int(ndb.latestVersion)) + + require.True(t, ndb.hasUpgradedToFastStorage()) +} + +func TestIsFastStorageEnabled_False(t *testing.T) { + db := dbm.NewMemDB() + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + ndb.latestVersion = 100 + ndb.storageVersion = defaultStorageVersionValue + + shouldForce, err := ndb.shouldForceFastStorageUpgrade() + require.False(t, shouldForce) + require.NoError(t, err) +} + +func TestTraverseNodes(t *testing.T) { + tree := getTestTree(0) + // version 1 + for i := 0; i < 20; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.NoError(t, err) + } + _, _, err := tree.SaveVersion() + require.NoError(t, err) + // version 2, no commit + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // version 3 + for i := 20; i < 30; i++ { + _, err := tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + count := 0 + err = tree.ndb.traverseNodes(func(node *Node) error { + actualNode, err := tree.ndb.GetNode(node.GetKey()) + if err != nil { + return err + } + if actualNode.String() != node.String() { + return fmt.Errorf("found unexpected node") + } + count++ + return nil + }) + require.NoError(t, err) + require.Equal(t, 64, count) +} + +func assertOrphansAndBranches(t *testing.T, ndb *nodeDB, version int64, branches int, orphanKeys [][]byte) { + var branchCount, orphanIndex int + err := ndb.traverseOrphans(version, version+1, func(node *Node) error { + if node.isLeaf() { + require.Equal(t, orphanKeys[orphanIndex], node.key) + orphanIndex++ + } else { + branchCount++ + } + return nil + }) + + require.NoError(t, err) + require.Equal(t, branches, branchCount) +} + +func TestNodeDB_traverseOrphans(t *testing.T) { + tree := getTestTree(0) + var up bool + var err error + + // version 1 + for i := 0; i < 20; i++ { + up, err = tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.False(t, up) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // note: assertions were constructed by hand after inspecting the output of the graphviz below. + // WriteDOTGraphToFile("/tmp/tree_one.dot", tree.ImmutableTree) + + // version 2 + up, err = tree.Set([]byte{byte(19)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_two.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 1, 5, [][]byte{{byte(19)}}) + + // version 3 + k, up, err := tree.Remove([]byte{byte(0)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_three.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 2, 4, [][]byte{{byte(0)}}) + + // version 4 + k, up, err = tree.Remove([]byte{byte(1)}) + require.Equal(t, []byte{byte(1)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(19)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_four.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 3, 7, [][]byte{{byte(1)}, {byte(19)}}) + + // version 5 + k, up, err = tree.Remove([]byte{byte(10)}) + require.Equal(t, []byte{byte(10)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(9)}) + require.Equal(t, []byte{byte(9)}, k) + require.True(t, up) + require.NoError(t, err) + up, err = tree.Set([]byte{byte(12)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_five.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 4, 8, [][]byte{{byte(9)}, {byte(10)}, {byte(12)}}) +} + +func makeAndPopulateMutableTree(tb testing.TB) *MutableTree { + memDB := dbm.NewMemDB() + tree := NewMutableTree(memDB, 0, false, log.NewNopLogger(), InitialVersionOption(9)) + + for i := 0; i < 1e4; i++ { + buf := make([]byte, 0, (i/255)+1) + for j := 0; 1<>j)&0xff)) + } + tree.Set(buf, buf) //nolint:errcheck + } + _, _, err := tree.SaveVersion() + require.Nil(tb, err, "Expected .SaveVersion to succeed") + return tree +} + +func TestDeleteVersionsFromNoDeadlock(t *testing.T) { + const expectedVersion = fastStorageVersionValue + + db := dbm.NewMemDB() + + ndb := newNodeDB(db, 0, DefaultOptions(), log.NewNopLogger()) + require.Equal(t, defaultStorageVersionValue, ndb.getStorageVersion()) + + err := ndb.SetFastStorageVersionToBatch(ndb.latestVersion) + require.NoError(t, err) + + latestVersion, err := ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, expectedVersion+fastStorageVersionDelimiter+strconv.Itoa(int(latestVersion)), ndb.getStorageVersion()) + require.NoError(t, ndb.batch.Write()) + + // Reported in https://github.com/cosmos/iavl/issues/842 + // there was a deadlock that triggered on an invalid version being + // checked for deletion. + // Now add in data to trigger the error path. + ndb.versionReaders[latestVersion+1] = 2 + + errCh := make(chan error) + targetVersion := latestVersion - 1 + + go func() { + defer close(errCh) + errCh <- ndb.DeleteVersionsFrom(targetVersion) + }() + + select { + case err = <-errCh: + // Happy path, the mutex was unlocked fast enough. + + case <-time.After(2 * time.Second): + t.Error("code did not return even after 2 seconds") + } + + require.True(t, ndb.mtx.TryLock(), "tryLock failed mutex was still locked") + ndb.mtx.Unlock() // Since TryLock passed, the lock is now solely being held by us. + require.Error(t, err, "") + require.Contains(t, err.Error(), fmt.Sprintf("unable to delete version %v with 2 active readers", targetVersion+2)) +} diff --git a/iavl/options.go b/iavl/options.go new file mode 100644 index 000000000..c679cfca1 --- /dev/null +++ b/iavl/options.go @@ -0,0 +1,120 @@ +package iavl + +import "sync/atomic" + +// Statisc about db runtime state +type Statistics struct { + // Each time GetNode operation hit cache + cacheHitCnt uint64 + + // Each time GetNode and GetFastNode operation miss cache + cacheMissCnt uint64 + + // Each time GetFastNode operation hit cache + fastCacheHitCnt uint64 + + // Each time GetFastNode operation miss cache + fastCacheMissCnt uint64 +} + +func (stat *Statistics) IncCacheHitCnt() { + if stat == nil { + return + } + atomic.AddUint64(&stat.cacheHitCnt, 1) +} + +func (stat *Statistics) IncCacheMissCnt() { + if stat == nil { + return + } + atomic.AddUint64(&stat.cacheMissCnt, 1) +} + +func (stat *Statistics) IncFastCacheHitCnt() { + if stat == nil { + return + } + atomic.AddUint64(&stat.fastCacheHitCnt, 1) +} + +func (stat *Statistics) IncFastCacheMissCnt() { + if stat == nil { + return + } + atomic.AddUint64(&stat.fastCacheMissCnt, 1) +} + +func (stat *Statistics) GetCacheHitCnt() uint64 { + return atomic.LoadUint64(&stat.cacheHitCnt) +} + +func (stat *Statistics) GetCacheMissCnt() uint64 { + return atomic.LoadUint64(&stat.cacheMissCnt) +} + +func (stat *Statistics) GetFastCacheHitCnt() uint64 { + return atomic.LoadUint64(&stat.fastCacheHitCnt) +} + +func (stat *Statistics) GetFastCacheMissCnt() uint64 { + return atomic.LoadUint64(&stat.fastCacheMissCnt) +} + +func (stat *Statistics) Reset() { + atomic.StoreUint64(&stat.cacheHitCnt, 0) + atomic.StoreUint64(&stat.cacheMissCnt, 0) + atomic.StoreUint64(&stat.fastCacheHitCnt, 0) + atomic.StoreUint64(&stat.fastCacheMissCnt, 0) +} + +// Options define tree options. +type Options struct { + // Sync synchronously flushes all writes to storage, using e.g. the fsync syscall. + // Disabling this significantly improves performance, but can lose data on e.g. power loss. + Sync bool + + // InitialVersion specifies the initial version number. If any versions already exist below + // this, an error is returned when loading the tree. Only used for the initial SaveVersion() + // call. + InitialVersion uint64 + + // When Stat is not nil, statistical logic needs to be executed + Stat *Statistics + + // Ethereum has found that commit of 100KB is optimal, ref ethereum/go-ethereum#15115 + FlushThreshold int +} + +// DefaultOptions returns the default options for IAVL. +func DefaultOptions() Options { + return Options{FlushThreshold: 100000} +} + +// SyncOption sets the Sync option. +func SyncOption(sync bool) Option { + return func(opts *Options) { + opts.Sync = sync + } +} + +// InitialVersionOption sets the initial version for the tree. +func InitialVersionOption(iv uint64) Option { + return func(opts *Options) { + opts.InitialVersion = iv + } +} + +// StatOption sets the Statistics for the tree. +func StatOption(stats *Statistics) Option { + return func(opts *Options) { + opts.Stat = stats + } +} + +// FlushThresholdOption sets the FlushThreshold for the batcher. +func FlushThresholdOption(ft int) Option { + return func(opts *Options) { + opts.FlushThreshold = ft + } +} diff --git a/iavl/proof.go b/iavl/proof.go new file mode 100644 index 000000000..4fdbc35e0 --- /dev/null +++ b/iavl/proof.go @@ -0,0 +1,239 @@ +package iavl + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "sync" + + hexbytes "github.com/cosmos/iavl/internal/bytes" + "github.com/cosmos/iavl/internal/encoding" +) + +var bufPool = &sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var ( + // ErrInvalidProof is returned by Verify when a proof cannot be validated. + ErrInvalidProof = fmt.Errorf("invalid proof") + + // ErrInvalidInputs is returned when the inputs passed to the function are invalid. + ErrInvalidInputs = fmt.Errorf("invalid inputs") + + // ErrInvalidRoot is returned when the root passed in does not match the proof's. + ErrInvalidRoot = fmt.Errorf("invalid root") +) + +//---------------------------------------- +// ProofInnerNode +// Contract: Left and Right can never both be set. Will result in a empty `[]` roothash + +type ProofInnerNode struct { + Height int8 `json:"height"` + Size int64 `json:"size"` + Version int64 `json:"version"` + Left []byte `json:"left"` + Right []byte `json:"right"` +} + +func (pin ProofInnerNode) String() string { + return pin.stringIndented("") +} + +func (pin ProofInnerNode) stringIndented(indent string) string { + return fmt.Sprintf(`ProofInnerNode{ +%s Height: %v +%s Size: %v +%s Version: %v +%s Left: %X +%s Right: %X +%s}`, + indent, pin.Height, + indent, pin.Size, + indent, pin.Version, + indent, pin.Left, + indent, pin.Right, + indent) +} + +func (pin ProofInnerNode) Hash(childHash []byte) ([]byte, error) { + hasher := sha256.New() + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + err := encoding.EncodeVarint(buf, int64(pin.Height)) + if err == nil { + err = encoding.EncodeVarint(buf, pin.Size) + } + if err == nil { + err = encoding.EncodeVarint(buf, pin.Version) + } + + if len(pin.Left) > 0 && len(pin.Right) > 0 { + return nil, errors.New("both left and right child hashes are set") + } + + if len(pin.Left) == 0 { + if err == nil { + err = encoding.EncodeBytes(buf, childHash) + } + if err == nil { + err = encoding.EncodeBytes(buf, pin.Right) + } + } else { + if err == nil { + err = encoding.EncodeBytes(buf, pin.Left) + } + if err == nil { + err = encoding.EncodeBytes(buf, childHash) + } + } + + if err != nil { + return nil, fmt.Errorf("failed to hash ProofInnerNode: %v", err) + } + + _, err = hasher.Write(buf.Bytes()) + if err != nil { + return nil, err + } + return hasher.Sum(nil), nil +} + +//---------------------------------------- + +type ProofLeafNode struct { + Key hexbytes.HexBytes `json:"key"` + ValueHash hexbytes.HexBytes `json:"value"` + Version int64 `json:"version"` +} + +func (pln ProofLeafNode) String() string { + return pln.stringIndented("") +} + +func (pln ProofLeafNode) stringIndented(indent string) string { + return fmt.Sprintf(`ProofLeafNode{ +%s Key: %v +%s ValueHash: %X +%s Version: %v +%s}`, + indent, pln.Key, + indent, pln.ValueHash, + indent, pln.Version, + indent) +} + +func (pln ProofLeafNode) Hash() ([]byte, error) { + hasher := sha256.New() + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + err := encoding.EncodeVarint(buf, 0) + if err == nil { + err = encoding.EncodeVarint(buf, 1) + } + if err == nil { + err = encoding.EncodeVarint(buf, pln.Version) + } + if err == nil { + err = encoding.EncodeBytes(buf, pln.Key) + } + if err == nil { + err = encoding.EncodeBytes(buf, pln.ValueHash) + } + if err != nil { + return nil, fmt.Errorf("failed to hash ProofLeafNode: %v", err) + } + _, err = hasher.Write(buf.Bytes()) + if err != nil { + return nil, err + } + + return hasher.Sum(nil), nil +} + +//---------------------------------------- + +// If the key does not exist, returns the path to the next leaf left of key (w/ +// path), except when key is less than the least item, in which case it returns +// a path to the least item. +func (node *Node) PathToLeaf(t *ImmutableTree, key []byte, version int64) (PathToLeaf, *Node, error) { + path := new(PathToLeaf) + val, err := node.pathToLeaf(t, key, version, path) + return *path, val, err +} + +// pathToLeaf is a helper which recursively constructs the PathToLeaf. +// As an optimization the already constructed path is passed in as an argument +// and is shared among recursive calls. +func (node *Node) pathToLeaf(t *ImmutableTree, key []byte, version int64, path *PathToLeaf) (*Node, error) { + if node.subtreeHeight == 0 { + if bytes.Equal(node.key, key) { + return node, nil + } + return node, errors.New("key does not exist") + } + + nodeVersion := version + if node.nodeKey != nil { + nodeVersion = node.nodeKey.version + } + // Note that we do not store the left child in the ProofInnerNode when we're going to add the + // left node as part of the path, similarly we don't store the right child info when going down + // the right child node. This is done as an optimization since the child info is going to be + // already stored in the next ProofInnerNode in PathToLeaf. + if bytes.Compare(key, node.key) < 0 { + // left side + rightNode, err := node.getRightNode(t) + if err != nil { + return nil, err + } + + pin := ProofInnerNode{ + Height: node.subtreeHeight, + Size: node.size, + Version: nodeVersion, + Left: nil, + Right: rightNode.hash, + } + *path = append(*path, pin) + + leftNode, err := node.getLeftNode(t) + if err != nil { + return nil, err + } + n, err := leftNode.pathToLeaf(t, key, version, path) + return n, err + } + // right side + leftNode, err := node.getLeftNode(t) + if err != nil { + return nil, err + } + + pin := ProofInnerNode{ + Height: node.subtreeHeight, + Size: node.size, + Version: nodeVersion, + Left: leftNode.hash, + Right: nil, + } + *path = append(*path, pin) + + rightNode, err := node.getRightNode(t) + if err != nil { + return nil, err + } + + n, err := rightNode.pathToLeaf(t, key, version, path) + return n, err +} diff --git a/iavl/proof_iavl_test.go b/iavl/proof_iavl_test.go new file mode 100644 index 000000000..50573bf68 --- /dev/null +++ b/iavl/proof_iavl_test.go @@ -0,0 +1,62 @@ +package iavl + +import ( + "fmt" + "testing" + + log "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +func TestProofOp(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + keys := []byte{0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7} // 10 total. + for _, ikey := range keys { + key := []byte{ikey} + _, err := tree.Set(key, key) + require.NoError(t, err) + } + + testcases := []struct { + key byte + expectPresent bool + }{ + {0x00, false}, + {0x0a, true}, + {0x0b, false}, + {0x11, true}, + {0x60, false}, + {0x72, true}, + {0x99, true}, + {0xaa, false}, + {0xe4, true}, + {0xf7, true}, + {0xff, false}, + } + + for _, tc := range testcases { + tc := tc + t.Run(fmt.Sprintf("%02x", tc.key), func(t *testing.T) { + key := []byte{tc.key} + if tc.expectPresent { + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err) + + // Verify that proof is valid. + res, err := tree.VerifyMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } else { + proof, err := tree.GetNonMembershipProof(key) + require.NoError(t, err) + + // Verify that proof is valid. + res, err := tree.VerifyNonMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } + }) + } +} diff --git a/iavl/proof_ics23.go b/iavl/proof_ics23.go new file mode 100644 index 000000000..913b6f7ec --- /dev/null +++ b/iavl/proof_ics23.go @@ -0,0 +1,212 @@ +package iavl + +import ( + "encoding/binary" + "fmt" + + ics23 "github.com/cosmos/ics23/go" +) + +/* +GetMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the iavl tree. +If the key doesn't exist in the tree, this will return an error. +*/ +func (t *ImmutableTree) GetMembershipProof(key []byte) (*ics23.CommitmentProof, error) { + exist, err := t.createExistenceProof(key) + if err != nil { + return nil, err + } + proof := &ics23.CommitmentProof{ + Proof: &ics23.CommitmentProof_Exist{ + Exist: exist, + }, + } + return proof, nil +} + +// VerifyMembership returns true iff proof is an ExistenceProof for the given key. +func (t *ImmutableTree) VerifyMembership(proof *ics23.CommitmentProof, key []byte) (bool, error) { + val, err := t.Get(key) + if err != nil { + return false, err + } + root := t.Hash() + + return ics23.VerifyMembership(ics23.IavlSpec, root, proof, key, val), nil +} + +/* +GetNonMembershipProof will produce a CommitmentProof that the given key doesn't exist in the iavl tree. +If the key exists in the tree, this will return an error. +*/ +func (t *ImmutableTree) GetNonMembershipProof(key []byte) (*ics23.CommitmentProof, error) { + // idx is one node right of what we want.... + var err error + idx, val, err := t.GetWithIndex(key) + if err != nil { + return nil, err + } + + if val != nil { + return nil, fmt.Errorf("cannot create NonExistanceProof when Key in State") + } + + nonexist := &ics23.NonExistenceProof{ + Key: key, + } + + if idx >= 1 { + leftkey, _, err := t.GetByIndex(idx - 1) + if err != nil { + return nil, err + } + + nonexist.Left, err = t.createExistenceProof(leftkey) + if err != nil { + return nil, err + } + } + + // this will be nil if nothing right of the queried key + rightkey, _, err := t.GetByIndex(idx) + if err != nil { + return nil, err + } + + if rightkey != nil { + nonexist.Right, err = t.createExistenceProof(rightkey) + if err != nil { + return nil, err + } + } + + proof := &ics23.CommitmentProof{ + Proof: &ics23.CommitmentProof_Nonexist{ + Nonexist: nonexist, + }, + } + return proof, nil +} + +// VerifyNonMembership returns true iff proof is a NonExistenceProof for the given key. +func (t *ImmutableTree) VerifyNonMembership(proof *ics23.CommitmentProof, key []byte) (bool, error) { + root := t.Hash() + + return ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key), nil +} + +// createExistenceProof will get the proof from the tree and convert the proof into a valid +// existence proof, if that's what it is. +func (t *ImmutableTree) createExistenceProof(key []byte) (*ics23.ExistenceProof, error) { + t.Hash() + path, node, err := t.root.PathToLeaf(t, key, t.version+1) + nodeVersion := t.version + 1 + if node.nodeKey != nil { + nodeVersion = node.nodeKey.version + } + return &ics23.ExistenceProof{ + Key: node.key, + Value: node.value, + Leaf: convertLeafOp(nodeVersion), + Path: convertInnerOps(path), + }, err +} + +func convertLeafOp(version int64) *ics23.LeafOp { + var varintBuf [binary.MaxVarintLen64]byte + // this is adapted from iavl/proof.go:proofLeafNode.Hash() + prefix := convertVarIntToBytes(0, varintBuf) + prefix = append(prefix, convertVarIntToBytes(1, varintBuf)...) + prefix = append(prefix, convertVarIntToBytes(version, varintBuf)...) + + return &ics23.LeafOp{ + Hash: ics23.HashOp_SHA256, + PrehashValue: ics23.HashOp_SHA256, + Length: ics23.LengthOp_VAR_PROTO, + Prefix: prefix, + } +} + +// we cannot get the proofInnerNode type, so we need to do the whole path in one function +func convertInnerOps(path PathToLeaf) []*ics23.InnerOp { + steps := make([]*ics23.InnerOp, 0, len(path)) + + // lengthByte is the length prefix prepended to each of the sha256 sub-hashes + var lengthByte byte = 0x20 + + var varintBuf [binary.MaxVarintLen64]byte + + // we need to go in reverse order, iavl starts from root to leaf, + // we want to go up from the leaf to the root + for i := len(path) - 1; i >= 0; i-- { + // this is adapted from iavl/proof.go:proofInnerNode.Hash() + prefix := convertVarIntToBytes(int64(path[i].Height), varintBuf) + prefix = append(prefix, convertVarIntToBytes(path[i].Size, varintBuf)...) + prefix = append(prefix, convertVarIntToBytes(path[i].Version, varintBuf)...) + + var suffix []byte + if len(path[i].Left) > 0 { + // length prefixed left side + prefix = append(prefix, lengthByte) + prefix = append(prefix, path[i].Left...) + // prepend the length prefix for child + prefix = append(prefix, lengthByte) + } else { + // prepend the length prefix for child + prefix = append(prefix, lengthByte) + // length-prefixed right side + suffix = []byte{lengthByte} + suffix = append(suffix, path[i].Right...) + } + + op := &ics23.InnerOp{ + Hash: ics23.HashOp_SHA256, + Prefix: prefix, + Suffix: suffix, + } + steps = append(steps, op) + } + return steps +} + +func convertVarIntToBytes(orig int64, buf [binary.MaxVarintLen64]byte) []byte { + n := binary.PutVarint(buf[:], orig) + return buf[:n] +} + +// GetProof gets the proof for the given key. +func (t *ImmutableTree) GetProof(key []byte) (*ics23.CommitmentProof, error) { + if t.root == nil { + return nil, fmt.Errorf("cannot generate the proof with nil root") + } + + exist, err := t.Has(key) + if err != nil { + return nil, err + } + + if exist { + return t.GetMembershipProof(key) + } + return t.GetNonMembershipProof(key) +} + +// VerifyProof checks if the proof is correct for the given key. +func (t *ImmutableTree) VerifyProof(proof *ics23.CommitmentProof, key []byte) (bool, error) { + if proof.GetExist() != nil { + return t.VerifyMembership(proof, key) + } + return t.VerifyNonMembership(proof, key) +} + +// GetVersionedProof gets the proof for the given key at the specified version. +func (tree *MutableTree) GetVersionedProof(key []byte, version int64) (*ics23.CommitmentProof, error) { + if tree.VersionExists(version) { + t, err := tree.GetImmutable(version) + if err != nil { + return nil, err + } + return t.GetProof(key) + } + return nil, ErrVersionDoesNotExist +} diff --git a/iavl/proof_ics23_test.go b/iavl/proof_ics23_test.go new file mode 100644 index 000000000..673c20730 --- /dev/null +++ b/iavl/proof_ics23_test.go @@ -0,0 +1,261 @@ +package iavl + +import ( + "bytes" + "crypto/rand" + mrand "math/rand" + "sort" + "testing" + + log "cosmossdk.io/log" + ics23 "github.com/cosmos/ics23/go" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" +) + +func TestGetMembership(t *testing.T) { + cases := map[string]struct { + size int + loc Where + }{ + "small left": {size: 100, loc: Left}, + "small middle": {size: 100, loc: Middle}, + "small right": {size: 100, loc: Right}, + "big left": {size: 5431, loc: Left}, + "big middle": {size: 5431, loc: Middle}, + "big right": {size: 5431, loc: Right}, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + tree, allkeys, err := BuildTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + + key := GetKey(allkeys, tc.loc) + val, err := tree.Get(key) + require.NoError(t, err) + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err, "Creating Proof: %+v", err) + + root := tree.WorkingHash() + valid := ics23.VerifyMembership(ics23.IavlSpec, root, proof, key, val) + require.True(t, valid, "Membership Proof Invalid") + }) + } +} + +func TestGetNonMembership(t *testing.T) { + cases := map[string]struct { + size int + loc Where + }{ + "small left": {size: 100, loc: Left}, + "small middle": {size: 100, loc: Middle}, + "small right": {size: 100, loc: Right}, + "big left": {size: 5431, loc: Left}, + "big middle": {size: 5431, loc: Middle}, + "big right": {size: 5431, loc: Right}, + } + + performTest := func(tree *MutableTree, allKeys [][]byte, loc Where) { + key := GetNonKey(allKeys, loc) + + proof, err := tree.GetNonMembershipProof(key) + require.NoError(t, err, "Creating Proof: %+v", err) + + root := tree.WorkingHash() + valid := ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key) + require.True(t, valid, "Non Membership Proof Invalid") + } + + for name, tc := range cases { + tc := tc + t.Run("fast-"+name, func(t *testing.T) { + tree, allkeys, err := BuildTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + // Save version to enable fast cache + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + performTest(tree, allkeys, tc.loc) + }) + + t.Run("regular-"+name, func(t *testing.T) { + tree, allkeys, err := BuildTree(tc.size, 0) + require.NoError(t, err, "Creating tree: %+v", err) + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(t, err) + require.False(t, isFastCacheEnabled) + + performTest(tree, allkeys, tc.loc) + }) + } +} + +func BenchmarkGetNonMembership(b *testing.B) { + cases := []struct { + size int + loc Where + }{ + {size: 100, loc: Left}, + {size: 100, loc: Middle}, + {size: 100, loc: Right}, + {size: 5431, loc: Left}, + {size: 5431, loc: Middle}, + {size: 5431, loc: Right}, + } + + performTest := func(tree *MutableTree, allKeys [][]byte, loc Where) { + key := GetNonKey(allKeys, loc) + + proof, err := tree.GetNonMembershipProof(key) + require.NoError(b, err, "Creating Proof: %+v", err) + + b.StopTimer() + root := tree.WorkingHash() + valid := ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key) + require.True(b, valid) + b.StartTimer() + } + + b.Run("fast", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + caseIdx := mrand.Intn(len(cases)) + tc := cases[caseIdx] + + tree, allkeys, err := BuildTree(tc.size, 100000) + require.NoError(b, err, "Creating tree: %+v", err) + // Save version to enable fast cache + _, _, err = tree.SaveVersion() + require.NoError(b, err) + + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + b.StartTimer() + performTest(tree, allkeys, tc.loc) + } + }) + + b.Run("regular", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + caseIdx := mrand.Intn(len(cases)) + tc := cases[caseIdx] + + tree, allkeys, err := BuildTree(tc.size, 100000) + require.NoError(b, err, "Creating tree: %+v", err) + isFastCacheEnabled, err := tree.IsFastCacheEnabled() + require.NoError(b, err) + require.False(b, isFastCacheEnabled) + + b.StartTimer() + performTest(tree, allkeys, tc.loc) + } + }) +} + +// Test Helpers + +// Where selects a location for a key - Left, Right, or Middle +type Where int + +const ( + Left Where = iota + Right + Middle +) + +// GetKey this returns a key, on Left/Right/Middle +func GetKey(allkeys [][]byte, loc Where) []byte { + if loc == Left { + return allkeys[0] + } + if loc == Right { + return allkeys[len(allkeys)-1] + } + // select a random index between 1 and allkeys-2 + idx := mrand.Int()%(len(allkeys)-2) + 1 + return allkeys[idx] +} + +// GetNonKey returns a missing key - Left of all, Right of all, or in the Middle +func GetNonKey(allkeys [][]byte, loc Where) []byte { + if loc == Left { + return []byte{0, 0, 0, 1} + } + if loc == Right { + return []byte{0xff, 0xff, 0xff, 0xff} + } + // otherwise, next to an existing key (copy before mod) + key := append([]byte{}, GetKey(allkeys, loc)...) + key[len(key)-2] = 255 + key[len(key)-1] = 255 + return key +} + +// BuildTree creates random key/values and stores in tree +// returns a list of all keys in sorted order +func BuildTree(size int, cacheSize int) (itree *MutableTree, keys [][]byte, err error) { + tree := NewMutableTree(dbm.NewMemDB(), cacheSize, false, log.NewNopLogger()) + + // insert lots of info and store the bytes + keys = make([][]byte, size) + for i := 0; i < size; i++ { + key := make([]byte, 4) + // create random 4 byte key + rand.Read(key) //nolint:errcheck + value := "value_for_key:" + string(key) + _, err = tree.Set(key, []byte(value)) + if err != nil { + return nil, nil, err + } + keys[i] = key + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i], keys[j]) < 0 + }) + + return tree, keys, nil +} + +// sink is kept as a global to ensure that value checks and assignments to it can't be +// optimized away, and this will help us ensure that benchmarks successfully run. +var sink interface{} + +func BenchmarkConvertLeafOp(b *testing.B) { + versions := []int64{ + 0, + 1, + 100, + 127, + 128, + 1 << 29, + -0, + -1, + -100, + -127, + -128, + -1 << 29, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, version := range versions { + sink = convertLeafOp(version) + } + } + if sink == nil { + b.Fatal("Benchmark wasn't run") + } + sink = nil +} diff --git a/iavl/proof_path.go b/iavl/proof_path.go new file mode 100644 index 000000000..19659500e --- /dev/null +++ b/iavl/proof_path.go @@ -0,0 +1,55 @@ +package iavl + +import ( + "fmt" + "strings" +) + +//---------------------------------------- + +// PathToLeaf represents an inner path to a leaf node. +// Note that the nodes are ordered such that the last one is closest +// to the root of the tree. +type PathToLeaf []ProofInnerNode + +func (pl PathToLeaf) String() string { + return pl.stringIndented("") +} + +func (pl PathToLeaf) stringIndented(indent string) string { + if len(pl) == 0 { + return "empty-PathToLeaf" + } + strs := make([]string, 0, len(pl)) + for i, pin := range pl { + if i == 20 { + strs = append(strs, fmt.Sprintf("... (%v total)", len(pl))) + break + } + strs = append(strs, fmt.Sprintf("%v:%v", i, pin.stringIndented(indent+" "))) + } + return fmt.Sprintf(`PathToLeaf{ +%s %v +%s}`, + indent, strings.Join(strs, "\n"+indent+" "), + indent) +} + +// returns -1 if invalid. +func (pl PathToLeaf) Index() (idx int64) { + for i, node := range pl { + switch { + case node.Left == nil: + continue + case node.Right == nil: + if i < len(pl)-1 { + idx += node.Size - pl[i+1].Size + } else { + idx += node.Size - 1 + } + default: + return -1 + } + } + return idx +} diff --git a/iavl/proof_test.go b/iavl/proof_test.go new file mode 100644 index 000000000..0c475c95b --- /dev/null +++ b/iavl/proof_test.go @@ -0,0 +1,128 @@ +// nolint: errcheck +package iavl + +import ( + "bytes" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +func TestTreeGetProof(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { + key := []byte{ikey} + tree.Set(key, []byte(iavlrand.RandStr(8))) + } + + key := []byte{0x32} + proof, err := tree.GetMembershipProof(key) + require.NoError(err) + require.NotNil(proof) + + res, err := tree.VerifyMembership(proof, key) + require.NoError(err, "%+v", err) + require.True(res) + + key = []byte{0x1} + proof, err = tree.GetNonMembershipProof(key) + require.NoError(err) + require.NotNil(proof) + + res, err = tree.VerifyNonMembership(proof, key) + require.NoError(err, "%+v", err) + require.True(res) +} + +func TestTreeKeyExistsProof(t *testing.T) { + tree := getTestTree(0) + + // should get error + _, err := tree.GetProof([]byte("foo")) + assert.Error(t, err) + + // insert lots of info and store the bytes + allkeys := make([][]byte, 200) + for i := 0; i < 200; i++ { + key := iavlrand.RandStr(20) + value := "value_for_" + key + tree.Set([]byte(key), []byte(value)) + allkeys[i] = []byte(key) + } + sortByteSlices(allkeys) // Sort all keys + + // query random key fails + _, err = tree.GetMembershipProof([]byte("foo")) + require.Error(t, err) + + // valid proof for real keys + for _, key := range allkeys { + proof, err := tree.GetMembershipProof(key) + require.NoError(t, err) + require.Equal(t, + append([]byte("value_for_"), key...), + proof.GetExist().Value, + ) + + res, err := tree.VerifyMembership(proof, key) + require.NoError(t, err) + require.True(t, res) + } +} + +//---------------------------------------- + +// Contract: !bytes.Equal(input, output) && len(input) >= len(output) +func MutateByteSlice(bytez []byte) []byte { + // If bytez is empty, panic + if len(bytez) == 0 { + panic("Cannot mutate an empty bytez") + } + + // Copy bytez + mBytez := make([]byte, len(bytez)) + copy(mBytez, bytez) + bytez = mBytez + + // Try a random mutation + switch iavlrand.RandInt() % 2 { + case 0: // Mutate a single byte + bytez[iavlrand.RandInt()%len(bytez)] += byte(iavlrand.RandInt()%255 + 1) + case 1: // Remove an arbitrary byte + pos := iavlrand.RandInt() % len(bytez) + bytez = append(bytez[:pos], bytez[pos+1:]...) + } + return bytez +} + +func sortByteSlices(src [][]byte) [][]byte { + bzz := byteslices(src) + sort.Sort(bzz) + return bzz +} + +type byteslices [][]byte + +func (bz byteslices) Len() int { + return len(bz) +} + +func (bz byteslices) Less(i, j int) bool { + switch bytes.Compare(bz[i], bz[j]) { + case -1: + return true + case 0, 1: + return false + default: + panic("should not happen") + } +} + +func (bz byteslices) Swap(i, j int) { + bz[j], bz[i] = bz[i], bz[j] +} diff --git a/iavl/proto/buf.lock b/iavl/proto/buf.lock new file mode 100644 index 000000000..c91b5810c --- /dev/null +++ b/iavl/proto/buf.lock @@ -0,0 +1,2 @@ +# Generated by buf. DO NOT EDIT. +version: v1 diff --git a/iavl/proto/buf.md b/iavl/proto/buf.md new file mode 100644 index 000000000..74cd1259a --- /dev/null +++ b/iavl/proto/buf.md @@ -0,0 +1,3 @@ +## IAVL + +Defines messages used by IAVL library, currently only `ChangeSet` and `KVPair`. diff --git a/iavl/proto/buf.yaml b/iavl/proto/buf.yaml new file mode 100644 index 000000000..f2008dbda --- /dev/null +++ b/iavl/proto/buf.yaml @@ -0,0 +1,10 @@ +version: v1 +name: buf.build/cosmos/iavl +breaking: + use: + - FILE +lint: + use: + - DEFAULT + - COMMENTS + - FILE_LOWER_SNAKE_CASE diff --git a/iavl/proto/changeset.pb.go b/iavl/proto/changeset.pb.go new file mode 100644 index 000000000..0146814c7 --- /dev/null +++ b/iavl/proto/changeset.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc (unknown) +// source: changeset.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type KVPair struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Delete bool `protobuf:"varint,1,opt,name=delete,proto3" json:"delete,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *KVPair) Reset() { + *x = KVPair{} + if protoimpl.UnsafeEnabled { + mi := &file_changeset_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KVPair) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KVPair) ProtoMessage() {} + +func (x *KVPair) ProtoReflect() protoreflect.Message { + mi := &file_changeset_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KVPair.ProtoReflect.Descriptor instead. +func (*KVPair) Descriptor() ([]byte, []int) { + return file_changeset_proto_rawDescGZIP(), []int{0} +} + +func (x *KVPair) GetDelete() bool { + if x != nil { + return x.Delete + } + return false +} + +func (x *KVPair) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *KVPair) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +type ChangeSet struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Pairs []*KVPair `protobuf:"bytes,1,rep,name=pairs,proto3" json:"pairs,omitempty"` +} + +func (x *ChangeSet) Reset() { + *x = ChangeSet{} + if protoimpl.UnsafeEnabled { + mi := &file_changeset_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeSet) ProtoMessage() {} + +func (x *ChangeSet) ProtoReflect() protoreflect.Message { + mi := &file_changeset_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeSet.ProtoReflect.Descriptor instead. +func (*ChangeSet) Descriptor() ([]byte, []int) { + return file_changeset_proto_rawDescGZIP(), []int{1} +} + +func (x *ChangeSet) GetPairs() []*KVPair { + if x != nil { + return x.Pairs + } + return nil +} + +var File_changeset_proto protoreflect.FileDescriptor + +var file_changeset_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x04, 0x69, 0x61, 0x76, 0x6c, 0x22, 0x48, 0x0a, 0x06, 0x4b, 0x56, 0x50, 0x61, 0x69, + 0x72, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0x2f, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x65, 0x74, 0x12, 0x22, + 0x0a, 0x05, 0x70, 0x61, 0x69, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x69, 0x61, 0x76, 0x6c, 0x2e, 0x4b, 0x56, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x70, 0x61, 0x69, + 0x72, 0x73, 0x42, 0x68, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x2e, 0x69, 0x61, 0x76, 0x6c, 0x42, 0x0e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x1c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x73, + 0x6d, 0x6f, 0x73, 0x2f, 0x69, 0x61, 0x76, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, + 0x03, 0x49, 0x58, 0x58, 0xaa, 0x02, 0x04, 0x49, 0x61, 0x76, 0x6c, 0xca, 0x02, 0x04, 0x49, 0x61, + 0x76, 0x6c, 0xe2, 0x02, 0x10, 0x49, 0x61, 0x76, 0x6c, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x04, 0x49, 0x61, 0x76, 0x6c, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_changeset_proto_rawDescOnce sync.Once + file_changeset_proto_rawDescData = file_changeset_proto_rawDesc +) + +func file_changeset_proto_rawDescGZIP() []byte { + file_changeset_proto_rawDescOnce.Do(func() { + file_changeset_proto_rawDescData = protoimpl.X.CompressGZIP(file_changeset_proto_rawDescData) + }) + return file_changeset_proto_rawDescData +} + +var file_changeset_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_changeset_proto_goTypes = []interface{}{ + (*KVPair)(nil), // 0: iavl.KVPair + (*ChangeSet)(nil), // 1: iavl.ChangeSet +} +var file_changeset_proto_depIdxs = []int32{ + 0, // 0: iavl.ChangeSet.pairs:type_name -> iavl.KVPair + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_changeset_proto_init() } +func file_changeset_proto_init() { + if File_changeset_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_changeset_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KVPair); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_changeset_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChangeSet); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_changeset_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_changeset_proto_goTypes, + DependencyIndexes: file_changeset_proto_depIdxs, + MessageInfos: file_changeset_proto_msgTypes, + }.Build() + File_changeset_proto = out.File + file_changeset_proto_rawDesc = nil + file_changeset_proto_goTypes = nil + file_changeset_proto_depIdxs = nil +} diff --git a/iavl/proto/changeset.proto b/iavl/proto/changeset.proto new file mode 100644 index 000000000..496f10aa3 --- /dev/null +++ b/iavl/proto/changeset.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package iavl; + +option go_package = "proto"; + +message KVPair { + bool delete = 1; + bytes key = 2; + bytes value = 3; +} + +message ChangeSet { + repeated KVPair pairs = 1; +} diff --git a/iavl/testdata/0.13-orphans-v6.db/000001.log b/iavl/testdata/0.13-orphans-v6.db/000001.log new file mode 100644 index 000000000..13bc49ab4 Binary files /dev/null and b/iavl/testdata/0.13-orphans-v6.db/000001.log differ diff --git a/iavl/testdata/0.13-orphans-v6.db/CURRENT b/iavl/testdata/0.13-orphans-v6.db/CURRENT new file mode 100644 index 000000000..feda7d6b2 --- /dev/null +++ b/iavl/testdata/0.13-orphans-v6.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/iavl/testdata/0.13-orphans-v6.db/LOCK b/iavl/testdata/0.13-orphans-v6.db/LOCK new file mode 100644 index 000000000..e69de29bb diff --git a/iavl/testdata/0.13-orphans-v6.db/LOG b/iavl/testdata/0.13-orphans-v6.db/LOG new file mode 100644 index 000000000..f890e80b8 --- /dev/null +++ b/iavl/testdata/0.13-orphans-v6.db/LOG @@ -0,0 +1,6 @@ +=============== Jun 25, 2020 (CEST) =============== +14:30:10.673317 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:30:10.688689 db@open opening +14:30:10.689548 version@stat F·[] S·0B[] Sc·[] +14:30:10.702481 db@janitor F·2 G·0 +14:30:10.702564 db@open done T·13.82376ms diff --git a/iavl/testdata/0.13-orphans-v6.db/MANIFEST-000000 b/iavl/testdata/0.13-orphans-v6.db/MANIFEST-000000 new file mode 100644 index 000000000..9d54f6733 Binary files /dev/null and b/iavl/testdata/0.13-orphans-v6.db/MANIFEST-000000 differ diff --git a/iavl/testdata/0.13-orphans.db/000001.log b/iavl/testdata/0.13-orphans.db/000001.log new file mode 100644 index 000000000..95ef16dca Binary files /dev/null and b/iavl/testdata/0.13-orphans.db/000001.log differ diff --git a/iavl/testdata/0.13-orphans.db/CURRENT b/iavl/testdata/0.13-orphans.db/CURRENT new file mode 100644 index 000000000..feda7d6b2 --- /dev/null +++ b/iavl/testdata/0.13-orphans.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/iavl/testdata/0.13-orphans.db/LOCK b/iavl/testdata/0.13-orphans.db/LOCK new file mode 100644 index 000000000..e69de29bb diff --git a/iavl/testdata/0.13-orphans.db/LOG b/iavl/testdata/0.13-orphans.db/LOG new file mode 100644 index 000000000..711c5a08f --- /dev/null +++ b/iavl/testdata/0.13-orphans.db/LOG @@ -0,0 +1,6 @@ +=============== Jun 25, 2020 (CEST) =============== +13:31:22.162368 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:31:22.173177 db@open opening +13:31:22.173961 version@stat F·[] S·0B[] Sc·[] +13:31:22.189072 db@janitor F·2 G·0 +13:31:22.189117 db@open done T·15.875399ms diff --git a/iavl/testdata/0.13-orphans.db/MANIFEST-000000 b/iavl/testdata/0.13-orphans.db/MANIFEST-000000 new file mode 100644 index 000000000..9d54f6733 Binary files /dev/null and b/iavl/testdata/0.13-orphans.db/MANIFEST-000000 differ diff --git a/iavl/testutils_test.go b/iavl/testutils_test.go new file mode 100644 index 000000000..b64a54984 --- /dev/null +++ b/iavl/testutils_test.go @@ -0,0 +1,358 @@ +// nolint:errcheck +package iavl + +import ( + "bytes" + "fmt" + "math/rand" + "runtime" + "sort" + "testing" + + log "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + "github.com/cosmos/iavl/internal/encoding" + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +type iteratorTestConfig struct { + startIterate, endIterate []byte + startByteToSet, endByteToSet byte + ascending bool +} + +func randstr(length int) string { + return iavlrand.RandStr(length) +} + +func i2b(i int) []byte { + buf := new(bytes.Buffer) + encoding.EncodeVarint(buf, int64(i)) + return buf.Bytes() +} + +func b2i(bz []byte) int { + i, _, err := encoding.DecodeVarint(bz) + if err != nil { + panic(err) + } + return int(i) +} + +// Construct a MutableTree +func getTestTree(cacheSize int) *MutableTree { + return NewMutableTree(dbm.NewMemDB(), cacheSize, false, log.NewNopLogger()) +} + +// Convenience for a new node +func N(l, r interface{}) *Node { + var left, right *Node + if _, ok := l.(*Node); ok { + left = l.(*Node) + } else { + left = NewNode(i2b(l.(int)), nil) + } + if _, ok := r.(*Node); ok { + right = r.(*Node) + } else { + right = NewNode(i2b(r.(int)), nil) + } + + n := &Node{ + key: right.lmd(nil).key, + value: nil, + leftNode: left, + rightNode: right, + } + n.calcHeightAndSize(nil) + return n +} + +// Setup a deep node +func T(n *Node) (*MutableTree, error) { + t := getTestTree(0) + + n.hashWithCount(t.version + 1) + t.root = n + return t, nil +} + +// Convenience for simple printing of keys & tree structure +func P(n *Node, t *ImmutableTree) string { + if n.subtreeHeight == 0 { + return fmt.Sprintf("%v", b2i(n.key)) + } + leftNode, _ := n.getLeftNode(t) + rightNode, _ := n.getRightNode(t) + return fmt.Sprintf("(%v %v)", P(leftNode, t), P(rightNode, t)) +} + +type traverser struct { + first string + last string + count int +} + +func (t *traverser) view(key, _ []byte) bool { + if t.first == "" { + t.first = string(key) + } + t.last = string(key) + t.count++ + return false +} + +func expectTraverse(t *testing.T, trav traverser, start, end string, count int) { + if trav.first != start { + t.Error("Bad start", start, trav.first) + } + if trav.last != end { + t.Error("Bad end", end, trav.last) + } + if trav.count != count { + t.Error("Bad count", count, trav.count) + } +} + +func assertMutableMirrorIterate(t *testing.T, tree *MutableTree, mirror map[string]string) { + sortedMirrorKeys := make([]string, 0, len(mirror)) + for k := range mirror { + sortedMirrorKeys = append(sortedMirrorKeys, k) + } + sort.Strings(sortedMirrorKeys) + + curKeyIdx := 0 + tree.Iterate(func(k, v []byte) bool { + nextMirrorKey := sortedMirrorKeys[curKeyIdx] + nextMirrorValue := mirror[nextMirrorKey] + + require.Equal(t, []byte(nextMirrorKey), k) + require.Equal(t, []byte(nextMirrorValue), v) + + curKeyIdx++ + return false + }) +} + +func assertImmutableMirrorIterate(t *testing.T, tree *ImmutableTree, mirror map[string]string) { + sortedMirrorKeys := getSortedMirrorKeys(mirror) + + curKeyIdx := 0 + tree.Iterate(func(k, v []byte) bool { + nextMirrorKey := sortedMirrorKeys[curKeyIdx] + nextMirrorValue := mirror[nextMirrorKey] + + require.Equal(t, []byte(nextMirrorKey), k) + require.Equal(t, []byte(nextMirrorValue), v) + + curKeyIdx++ + return false + }) +} + +func getSortedMirrorKeys(mirror map[string]string) []string { + sortedMirrorKeys := make([]string, 0, len(mirror)) + for k := range mirror { + sortedMirrorKeys = append(sortedMirrorKeys, k) + } + sort.Strings(sortedMirrorKeys) + return sortedMirrorKeys +} + +func getRandomizedTreeAndMirror(t *testing.T) (*MutableTree, map[string]string) { + const cacheSize = 100 + + tree := getTestTree(cacheSize) + + mirror := make(map[string]string) + + randomizeTreeAndMirror(t, tree, mirror) + return tree, mirror +} + +func randomizeTreeAndMirror(t *testing.T, tree *MutableTree, mirror map[string]string) { + if mirror == nil { + mirror = make(map[string]string) + } + const keyValLength = 5 + + numberOfSets := 1000 + numberOfUpdates := numberOfSets / 4 + numberOfRemovals := numberOfSets / 4 + + for numberOfSets > numberOfRemovals*3 { + key := iavlrand.RandBytes(keyValLength) + value := iavlrand.RandBytes(keyValLength) + + isUpdated, err := tree.Set(key, value) + require.NoError(t, err) + require.False(t, isUpdated) + mirror[string(key)] = string(value) + + numberOfSets-- + } + + for numberOfSets+numberOfRemovals+numberOfUpdates > 0 { + randOp := rand.Intn(3) + + switch randOp { + case 0: + if numberOfSets == 0 { + continue + } + + numberOfSets-- + + key := iavlrand.RandBytes(keyValLength) + value := iavlrand.RandBytes(keyValLength) + + isUpdated, err := tree.Set(key, value) + require.NoError(t, err) + require.False(t, isUpdated) + mirror[string(key)] = string(value) + case 1: + + if numberOfUpdates == 0 { + continue + } + numberOfUpdates-- + + key := getRandomKeyFrom(mirror) + value := iavlrand.RandBytes(keyValLength) + + isUpdated, err := tree.Set([]byte(key), value) + require.NoError(t, err) + require.True(t, isUpdated) + mirror[key] = string(value) + case 2: + if numberOfRemovals == 0 { + continue + } + numberOfRemovals-- + + key := getRandomKeyFrom(mirror) + + val, isRemoved, err := tree.Remove([]byte(key)) + require.NoError(t, err) + require.True(t, isRemoved) + require.NotNil(t, val) + delete(mirror, key) + default: + t.Error("Invalid randOp", randOp) + } + } +} + +func getRandomKeyFrom(mirror map[string]string) string { + for k := range mirror { + return k + } + panic("no keys in mirror") +} + +func setupMirrorForIterator(t *testing.T, config *iteratorTestConfig, tree *MutableTree) [][]string { + var mirror [][]string + + startByteToSet := config.startByteToSet + endByteToSet := config.endByteToSet + + if !config.ascending { + startByteToSet, endByteToSet = endByteToSet, startByteToSet + } + + curByte := startByteToSet + for curByte != endByteToSet { + value := iavlrand.RandBytes(5) + + if (config.startIterate == nil || curByte >= config.startIterate[0]) && (config.endIterate == nil || curByte < config.endIterate[0]) { + mirror = append(mirror, []string{string(curByte), string(value)}) + } + + isUpdated, err := tree.Set([]byte{curByte}, value) + require.NoError(t, err) + require.False(t, isUpdated) + + if config.ascending { + curByte++ + } else { + curByte-- + } + } + return mirror +} + +// assertIterator confirms that the iterator returns the expected values desribed by mirror in the same order. +// mirror is a slice containing slices of the form [key, value]. In other words, key at index 0 and value at index 1. +func assertIterator(t *testing.T, itr dbm.Iterator, mirror [][]string, ascending bool) { + startIdx, endIdx := 0, len(mirror)-1 + increment := 1 + mirrorIdx := startIdx + + // flip the iteration order over mirror if descending + if !ascending { + startIdx = endIdx - 1 + endIdx = -1 + increment *= -1 + } + + for startIdx != endIdx { + nextExpectedPair := mirror[mirrorIdx] + + require.True(t, itr.Valid()) + require.Equal(t, []byte(nextExpectedPair[0]), itr.Key()) + require.Equal(t, []byte(nextExpectedPair[1]), itr.Value()) + itr.Next() + require.NoError(t, itr.Error()) + + startIdx += increment + mirrorIdx++ + } +} + +func BenchmarkImmutableAvlTreeMemDB(b *testing.B) { + db := dbm.NewMemDB() + benchmarkImmutableAvlTreeWithDB(b, db) +} + +func benchmarkImmutableAvlTreeWithDB(b *testing.B, db dbm.DB) { + defer db.Close() + + b.StopTimer() + + t := NewMutableTree(db, 100000, false, log.NewNopLogger()) + + value := []byte{} + for i := 0; i < 1000000; i++ { + t.Set(i2b(int(iavlrand.RandInt31())), value) + if i > 990000 && i%1000 == 999 { + t.SaveVersion() + } + } + b.ReportAllocs() + t.SaveVersion() + + runtime.GC() + + b.StartTimer() + for i := 0; i < b.N; i++ { + ri := i2b(int(iavlrand.RandInt31())) + t.Set(ri, value) + t.Remove(ri) + if i%100 == 99 { + t.SaveVersion() + } + } +} + +func (node *Node) lmd(t *ImmutableTree) *Node { + if node.isLeaf() { + return node + } + + // TODO: Should handle this error? + leftNode, _ := node.getLeftNode(t) + + return leftNode.lmd(t) +} diff --git a/iavl/tree_dotgraph.go b/iavl/tree_dotgraph.go new file mode 100644 index 000000000..f86c2ea1e --- /dev/null +++ b/iavl/tree_dotgraph.go @@ -0,0 +1,177 @@ +package iavl + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "text/template" + + "github.com/emicklei/dot" +) + +type graphEdge struct { + From, To string +} + +type graphNode struct { + Hash string + Label string + Value string + Attrs map[string]string +} + +type graphContext struct { + Edges []*graphEdge + Nodes []*graphNode +} + +var graphTemplate = ` +strict graph { + {{- range $i, $edge := $.Edges}} + "{{ $edge.From }}" -- "{{ $edge.To }}"; + {{- end}} + + {{range $i, $node := $.Nodes}} + "{{ $node.Hash }}" [label=<{{ $node.Label }}>,{{ range $k, $v := $node.Attrs }}{{ $k }}={{ $v }},{{end}}]; + {{- end}} +} +` + +var tpl = template.Must(template.New("iavl").Parse(graphTemplate)) + +var defaultGraphNodeAttrs = map[string]string{ + "shape": "circle", +} + +func WriteDOTGraph(w io.Writer, tree *ImmutableTree, paths []PathToLeaf) { + ctx := &graphContext{} + + // TODO: handle error + tree.root.hashWithCount(tree.version + 1) + tree.root.traverse(tree, true, func(node *Node) bool { + graphNode := &graphNode{ + Attrs: map[string]string{}, + Hash: fmt.Sprintf("%x", node.hash), + } + for k, v := range defaultGraphNodeAttrs { + graphNode.Attrs[k] = v + } + shortHash := graphNode.Hash[:7] + + graphNode.Label = mkLabel(string(node.key), 16, "sans-serif") + graphNode.Label += mkLabel(shortHash, 10, "monospace") + graphNode.Label += mkLabel(fmt.Sprintf("nodeKey=%v", node.nodeKey), 10, "monospace") + + if node.value != nil { + graphNode.Label += mkLabel(string(node.value), 10, "sans-serif") + } + + if node.subtreeHeight == 0 { + graphNode.Attrs["fillcolor"] = "lightgrey" + graphNode.Attrs["style"] = "filled" + } + + for _, path := range paths { + for _, n := range path { + if bytes.Equal(n.Left, node.hash) || bytes.Equal(n.Right, node.hash) { + graphNode.Attrs["peripheries"] = "2" + graphNode.Attrs["style"] = "filled" + graphNode.Attrs["fillcolor"] = "lightblue" + break + } + } + } + ctx.Nodes = append(ctx.Nodes, graphNode) + + if node.leftNode != nil { + ctx.Edges = append(ctx.Edges, &graphEdge{ + From: graphNode.Hash, + To: fmt.Sprintf("%x", node.leftNode.hash), + }) + } + if node.rightNode != nil { + ctx.Edges = append(ctx.Edges, &graphEdge{ + From: graphNode.Hash, + To: fmt.Sprintf("%x", node.rightNode.hash), + }) + } + return false + }) + + if err := tpl.Execute(w, ctx); err != nil { + panic(err) + } +} + +func mkLabel(label string, pt int, face string) string { + return fmt.Sprintf("%s
", face, pt, label) +} + +// WriteDOTGraphToFile writes the DOT graph to the given filename. Read like: +// $ dot /tmp/tree_one.dot -Tpng | display +func WriteDOTGraphToFile(filename string, tree *ImmutableTree) { + f1, _ := os.Create(filename) + defer f1.Close() + writer := bufio.NewWriter(f1) + WriteDotGraphv2(writer, tree) + err := writer.Flush() + if err != nil { + panic(err) + } +} + +// WriteDotGraphv2 writes a DOT graph to the given writer. WriteDOTGraph failed to produce valid DOT +// graphs for large trees. This function is a rewrite of WriteDOTGraph that produces valid DOT graphs +func WriteDotGraphv2(w io.Writer, tree *ImmutableTree) { + graph := dot.NewGraph(dot.Directed) + + var traverse func(node *Node, parent *dot.Node, direction string) + traverse = func(node *Node, parent *dot.Node, direction string) { + var label string + if node.isLeaf() { + label = fmt.Sprintf("%v:%v\nv%v", node.key, node.value, node.nodeKey.version) + } else { + label = fmt.Sprintf("%v:%v\nv%v", node.subtreeHeight, node.key, node.nodeKey.version) + } + + n := graph.Node(label) + if parent != nil { + parent.Edge(n, direction) + } + + var leftNode, rightNode *Node + + if node.leftNode != nil { + leftNode = node.leftNode + } else if node.leftNodeKey != nil { + in, err := node.getLeftNode(tree) + if err == nil { + leftNode = in + } + } + + if node.rightNode != nil { + rightNode = node.rightNode + } else if node.rightNodeKey != nil { + in, err := node.getRightNode(tree) + if err == nil { + rightNode = in + } + } + + if leftNode != nil { + traverse(leftNode, &n, "l") + } + if rightNode != nil { + traverse(rightNode, &n, "r") + } + } + + traverse(tree.root, nil, "") + _, err := w.Write([]byte(graph.String())) + if err != nil { + panic(err) + } +} diff --git a/iavl/tree_dotgraph_test.go b/iavl/tree_dotgraph_test.go new file mode 100644 index 000000000..41efdfcbe --- /dev/null +++ b/iavl/tree_dotgraph_test.go @@ -0,0 +1,17 @@ +package iavl + +import ( + "io" + "testing" +) + +func TestWriteDOTGraph(_ *testing.T) { + tree := getTestTree(0) + for _, ikey := range []byte{ + 0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7, + } { + key := []byte{ikey} + tree.Set(key, key) //nolint:errcheck + } + WriteDOTGraph(io.Discard, tree.ImmutableTree, []PathToLeaf{}) +} diff --git a/iavl/tree_fuzz_test.go b/iavl/tree_fuzz_test.go new file mode 100644 index 000000000..4abe43162 --- /dev/null +++ b/iavl/tree_fuzz_test.go @@ -0,0 +1,127 @@ +// nolint:errcheck +package iavl + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +// This file implement fuzz testing by generating programs and then running +// them. If an error occurs, the program that had the error is printed. + +// A program is a list of instructions. +type program struct { + instructions []instruction +} + +func (p *program) Execute(tree *MutableTree) (err error) { + var errLine int + + defer func() { + if r := recover(); r != nil { + var str string + + for i, instr := range p.instructions { + prefix := " " + if i == errLine { + prefix = ">> " + } + str += prefix + instr.String() + "\n" + } + err = fmt.Errorf("program panicked with: %s\n%s", r, str) + } + }() + + for i, instr := range p.instructions { + errLine = i + instr.Execute(tree) + } + return +} + +func (p *program) addInstruction(i instruction) { + p.instructions = append(p.instructions, i) +} + +func (p *program) size() int { + return len(p.instructions) +} + +type instruction struct { + op string + k, v []byte + version int64 +} + +func (i instruction) Execute(tree *MutableTree) { + switch i.op { + case "SET": + tree.Set(i.k, i.v) + case "REMOVE": + tree.Remove(i.k) + case "SAVE": + tree.SaveVersion() + case "DELETE": + tree.DeleteVersionsTo(i.version) + default: + panic("Unrecognized op: " + i.op) + } +} + +func (i instruction) String() string { + if i.version > 0 { + return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.op, i.k, i.v, i.version) + } + return fmt.Sprintf("%-8s %-8s %-8s", i.op, i.k, i.v) +} + +// Generate a random program of the given size. +func genRandomProgram(size int) *program { + p := &program{} + nextVersion := 1 + + for p.size() < size { + k, v := []byte(iavlrand.RandStr(1)), []byte(iavlrand.RandStr(1)) + + switch rand.Int() % 7 { + case 0, 1, 2: + p.addInstruction(instruction{op: "SET", k: k, v: v}) + case 3, 4: + p.addInstruction(instruction{op: "REMOVE", k: k}) + case 5: + p.addInstruction(instruction{op: "SAVE", version: int64(nextVersion)}) + nextVersion++ + case 6: + if rv := rand.Int() % nextVersion; rv < nextVersion && rv > 0 { + p.addInstruction(instruction{op: "DELETE", version: int64(rv)}) + } + } + } + return p +} + +// Generate many programs and run them. +func TestMutableTreeFuzz(t *testing.T) { + maxIterations := testFuzzIterations + progsPerIteration := 100000 + iterations := 0 + + for size := 5; iterations < maxIterations; size++ { + for i := 0; i < progsPerIteration/size; i++ { + tree := getTestTree(0) + program := genRandomProgram(size) + err := program.Execute(tree) + if err != nil { + str, err := tree.String() + require.Nil(t, err) + t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), str) + } + iterations++ + } + } +} diff --git a/iavl/tree_random_test.go b/iavl/tree_random_test.go new file mode 100644 index 000000000..08739d432 --- /dev/null +++ b/iavl/tree_random_test.go @@ -0,0 +1,429 @@ +package iavl + +import ( + "encoding/base64" + "fmt" + "math/rand" + "os" + "sort" + "strconv" + "strings" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + "github.com/cosmos/iavl/fastnode" +) + +func TestRandomOperations(t *testing.T) { + // In short mode (specifically, when running in CI with the race detector), + // we only run the first couple of seeds. + seeds := []int64{ + 498727689, + 756509998, + 480459882, + 324736440, + 581827344, + 470870060, + 390970079, + 846023066, + 518638291, + 957382170, + } + + for i, seed := range seeds { + i, seed := i, seed + t.Run(fmt.Sprintf("Seed %v", seed), func(t *testing.T) { + if testing.Short() && i >= 2 { + t.Skip("Skipping seed in short mode") + } + t.Parallel() // comment out to disable parallel tests, or use -parallel 1 + testRandomOperations(t, seed) + }) + } +} + +// Randomized test that runs all sorts of random operations, mirrors them in a known-good +// map, and verifies the state of the tree against the map. +func testRandomOperations(t *testing.T, randSeed int64) { + const ( + keySize = 16 // before base64-encoding + valueSize = 16 // before base64-encoding + + versions = 32 // number of final versions to generate + reloadChance = 0.1 // chance of tree reload after save + deleteChance = 0.2 // chance of random version deletion after save + revertChance = 0.05 // chance to revert tree to random version with LoadVersionForOverwriting + syncChance = 0.2 // chance of enabling sync writes on tree load + cacheChance = 0.4 // chance of enabling caching + cacheSizeMax = 256 // maximum size of cache (will be random from 1) + + versionOps = 64 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + r := rand.New(rand.NewSource(randSeed)) + + // loadTree loads the last persisted version of a tree with random pruning settings. + loadTree := func(levelDB dbm.DB) (tree *MutableTree, version int64, _ *Options) { //nolint:unparam + var err error + + sync := r.Float64() < syncChance + + // set the cache size regardless of whether caching is enabled. This ensures we always + // call the RNG the same number of times, such that changing settings does not affect + // the RNG sequence. + cacheSize := int(r.Int63n(cacheSizeMax + 1)) + if !(r.Float64() < cacheChance) { + cacheSize = 0 + } + tree = NewMutableTree(levelDB, cacheSize, false, log.NewNopLogger(), SyncOption(sync)) + version, err = tree.Load() + require.NoError(t, err) + t.Logf("Loaded version %v (sync=%v cache=%v)", version, sync, cacheSize) + return + } + + // generates random keys and values + randString := func(size int) string { //nolint:unparam + buf := make([]byte, size) + r.Read(buf) + return base64.StdEncoding.EncodeToString(buf) + } + + // Use the same on-disk database for the entire run. + tempdir, err := os.MkdirTemp("", "iavl") + require.NoError(t, err) + defer os.RemoveAll(tempdir) + + levelDB, err := dbm.NewDB("test", "goleveldb", tempdir) + require.NoError(t, err) + + tree, version, _ := loadTree(levelDB) + + // Set up a mirror of the current IAVL state, as well as the history of saved mirrors + // on disk and in memory. Since pruning was removed we currently persist all versions, + // thus memMirrors is never used, but it is left here for the future when it is re-introduces. + mirror := make(map[string]string, versionOps) + mirrorKeys := make([]string, 0, versionOps) + diskMirrors := make(map[int64]map[string]string) + memMirrors := make(map[int64]map[string]string) + + for version < versions { + for i := 0; i < versionOps; i++ { + switch { + case len(mirror) > 0 && r.Float64() < deleteRatio: + index := r.Intn(len(mirrorKeys)) + key := mirrorKeys[index] + mirrorKeys = append(mirrorKeys[:index], mirrorKeys[index+1:]...) + _, removed, err := tree.Remove([]byte(key)) + require.NoError(t, err) + require.True(t, removed) + delete(mirror, key) + + case len(mirror) > 0 && r.Float64() < updateRatio: + key := mirrorKeys[r.Intn(len(mirrorKeys))] + value := randString(valueSize) + updated, err := tree.Set([]byte(key), []byte(value)) + require.NoError(t, err) + require.True(t, updated) + mirror[key] = value + + default: + key := randString(keySize) + value := randString(valueSize) + for has, err := tree.Has([]byte(key)); has && err == nil; { + key = randString(keySize) + } + updated, err := tree.Set([]byte(key), []byte(value)) + require.NoError(t, err) + require.False(t, updated) + mirror[key] = value + mirrorKeys = append(mirrorKeys, key) + } + } + _, version, err = tree.SaveVersion() + require.NoError(t, err) + + t.Logf("Saved tree at version %v with %v keys and %v versions", + version, tree.Size(), len(tree.AvailableVersions())) + + // Verify that the version matches the mirror. + assertMirror(t, tree, mirror, 0) + + // Save the mirror as a disk mirror, since we currently persist all versions. + diskMirrors[version] = copyMirror(mirror) + + // Delete random versions if requested, but never the latest version. + if r.Float64() < deleteChance { + versions := getMirrorVersions(diskMirrors, memMirrors) + if len(versions) > 1 { + to := versions[r.Intn(len(versions)-1)] + t.Logf("Deleting versions to %v", to) + err = tree.DeleteVersionsTo(int64(to)) + require.NoError(t, err) + for version := versions[0]; version <= to; version++ { + delete(diskMirrors, int64(version)) + delete(memMirrors, int64(version)) + } + } + } + + // Reload tree from last persisted version if requested, checking that it matches the + // latest disk mirror version and discarding memory mirrors. + if r.Float64() < reloadChance { + tree, version, _ = loadTree(levelDB) + assertMaxVersion(t, tree, version, diskMirrors) + memMirrors = make(map[int64]map[string]string) + mirror = copyMirror(diskMirrors[version]) + mirrorKeys = getMirrorKeys(mirror) + } + + // Revert tree to historical version if requested, deleting all subsequent versions. + if r.Float64() < revertChance { + versions := getMirrorVersions(diskMirrors, memMirrors) + if len(versions) > 1 { + version = int64(versions[r.Intn(len(versions)-1)]) + t.Logf("Reverting to version %v", version) + err = tree.LoadVersionForOverwriting(version) + require.NoError(t, err, "Failed to revert to version %v", version) + if m, ok := diskMirrors[version]; ok { + mirror = copyMirror(m) + } else if m, ok := memMirrors[version]; ok { + mirror = copyMirror(m) + } else { + t.Fatalf("Mirror not found for revert target %v", version) + } + mirrorKeys = getMirrorKeys(mirror) + for v := range diskMirrors { + if v > version { + delete(diskMirrors, v) + } + } + for v := range memMirrors { + if v > version { + delete(memMirrors, v) + } + } + } + } + + // Verify all historical versions. + assertVersions(t, tree, diskMirrors, memMirrors) + + for diskVersion, diskMirror := range diskMirrors { + assertMirror(t, tree, diskMirror, diskVersion) + } + + for memVersion, memMirror := range memMirrors { + assertMirror(t, tree, memMirror, memVersion) + } + } + + // Once we're done, delete all prior versions. + remaining := tree.AvailableVersions() + remaining = remaining[:len(remaining)-1] + + if len(remaining) > 0 { + t.Logf("Deleting versions to %v", remaining[len(remaining)-1]) + err = tree.DeleteVersionsTo(int64(remaining[len(remaining)-1])) + require.NoError(t, err) + } + + require.EqualValues(t, []int{int(version)}, tree.AvailableVersions()) + assertMirror(t, tree, mirror, version) + assertMirror(t, tree, mirror, 0) + assertOrphans(t, tree, 0) + t.Logf("Final version %v is correct, with no stray orphans", version) + + // Now, let's delete all remaining key/value pairs, and make sure no stray + // data is left behind in the database. + prevVersion := tree.Version() + keys := [][]byte{} + _, err = tree.Iterate(func(key, value []byte) bool { + keys = append(keys, key) + return false + }) + require.NoError(t, err) + for _, key := range keys { + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + err = tree.DeleteVersionsTo(prevVersion) + require.NoError(t, err) + assertEmptyDatabase(t, tree) + t.Logf("Final version %v deleted, no stray database entries", prevVersion) +} + +// Checks that the database is empty, only containing a single root entry +// at the given version. +func assertEmptyDatabase(t *testing.T, tree *MutableTree) { + version := tree.Version() + iter, err := tree.ndb.db.Iterator(nil, nil) + require.NoError(t, err) + + var foundKeys []string + for ; iter.Valid(); iter.Next() { + foundKeys = append(foundKeys, string(iter.Key())) + } + require.NoError(t, iter.Error()) + require.EqualValues(t, 2, len(foundKeys), "Found %v database entries, expected 1", len(foundKeys)) // 1 for storage version and 1 for root + + firstKey := foundKeys[0] + secondKey := foundKeys[1] + require.True(t, strings.HasPrefix(firstKey, metadataKeyFormat.Prefix())) + + require.Equal(t, string(metadataKeyFormat.KeyBytes([]byte(storageVersionKey))), firstKey, "Unexpected storage version key") + + storageVersionValue, err := tree.ndb.db.Get([]byte(firstKey)) + require.NoError(t, err) + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + require.Equal(t, fastStorageVersionValue+fastStorageVersionDelimiter+strconv.Itoa(int(latestVersion)), string(storageVersionValue)) + + var foundVersion int64 + nodeKeyFormat.Scan([]byte(secondKey), &foundVersion) + require.Equal(t, version, foundVersion, "Unexpected root version") +} + +// Checks that the tree has the given number of orphan nodes. +func assertOrphans(t *testing.T, tree *MutableTree, expected int) { + orphans, err := tree.ndb.orphans() + require.Nil(t, err) + require.EqualValues(t, expected, len(orphans), "Expected %v orphans, got %v", expected, len(orphans)) +} + +// Checks that a version is the maximum mirrored version. +func assertMaxVersion(t *testing.T, _ *MutableTree, version int64, mirrors map[int64]map[string]string) { + max := int64(0) + for v := range mirrors { + if v > max { + max = v + } + } + require.Equal(t, max, version) +} + +// Checks that a mirror, optionally for a given version, matches the tree contents. +func assertMirror(t *testing.T, tree *MutableTree, mirror map[string]string, version int64) { + var err error + itree := tree.ImmutableTree + if version > 0 { + itree, err = tree.GetImmutable(version) + require.NoError(t, err, "loading version %v", version) + } + // We check both ways: first check that iterated keys match the mirror, then iterate over the + // mirror and check with get. This is to exercise both the iteration and Get() code paths. + iterated := 0 + _, err = itree.Iterate(func(key, value []byte) bool { + if string(value) != mirror[string(key)] { + fmt.Println("missing ", string(key), " ", string(value)) + } + require.Equal(t, string(value), mirror[string(key)], "Invalid value for key %q", key) + iterated++ + return false + }) + require.NoError(t, err) + require.EqualValues(t, len(mirror), itree.Size()) + require.EqualValues(t, len(mirror), iterated) + for key, value := range mirror { + actualFast, err := itree.Get([]byte(key)) + require.NoError(t, err) + require.Equal(t, value, string(actualFast)) + _, actual, err := itree.GetWithIndex([]byte(key)) + require.NoError(t, err) + require.Equal(t, value, string(actual)) + } + + assertFastNodeCacheIsLive(t, tree, mirror, version) + assertFastNodeDiskIsLive(t, tree, mirror, version) +} + +// Checks that fast node cache matches live state. +func assertFastNodeCacheIsLive(t *testing.T, tree *MutableTree, mirror map[string]string, version int64) { + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + if latestVersion != version { + // The fast node cache check should only be done to the latest version + return + } + + require.Equal(t, len(mirror), tree.ndb.fastNodeCache.Len()) + for k, v := range mirror { + require.True(t, tree.ndb.fastNodeCache.Has([]byte(k)), "cached fast node must be in live tree") + mirrorNode := tree.ndb.fastNodeCache.Get([]byte(k)) + require.Equal(t, []byte(v), mirrorNode.(*fastnode.Node).GetValue(), "cached fast node's value must be equal to live state value") + } +} + +// Checks that fast nodes on disk match live state. +func assertFastNodeDiskIsLive(t *testing.T, tree *MutableTree, mirror map[string]string, version int64) { + latestVersion, err := tree.ndb.getLatestVersion() + require.NoError(t, err) + if latestVersion != version { + // The fast node disk check should only be done to the latest version + return + } + + count := 0 + err = tree.ndb.traverseFastNodes(func(keyWithPrefix, v []byte) error { + key := keyWithPrefix[1:] + count++ + fastNode, err := fastnode.DeserializeNode(key, v) + require.Nil(t, err) + + mirrorVal := mirror[string(fastNode.GetKey())] + + require.NotNil(t, mirrorVal) + require.Equal(t, []byte(mirrorVal), fastNode.GetValue()) + return nil + }) + require.NoError(t, err) + require.Equal(t, len(mirror), count) +} + +// Checks that all versions in the tree are present in the mirrors, and vice-versa. +func assertVersions(t *testing.T, tree *MutableTree, mirrors ...map[int64]map[string]string) { + require.Equal(t, getMirrorVersions(mirrors...), tree.AvailableVersions()) +} + +// copyMirror copies a mirror map. +func copyMirror(mirror map[string]string) map[string]string { + c := make(map[string]string, len(mirror)) + for k, v := range mirror { + c[k] = v + } + return c +} + +// getMirrorKeys returns the keys of a mirror, unsorted. +func getMirrorKeys(mirror map[string]string) []string { + keys := make([]string, 0, len(mirror)) + for key := range mirror { + keys = append(keys, key) + } + return keys +} + +// getMirrorVersions returns the versions of the given mirrors, sorted. Returns []int to +// match tree.AvailableVersions(). +func getMirrorVersions(mirrors ...map[int64]map[string]string) []int { + versionMap := make(map[int]bool) + for _, m := range mirrors { + for version := range m { + versionMap[int(version)] = true + } + } + versions := make([]int, 0, len(versionMap)) + for version := range versionMap { + versions = append(versions, version) + } + sort.Ints(versions) + return versions +} diff --git a/iavl/tree_test.go b/iavl/tree_test.go new file mode 100644 index 000000000..620a59048 --- /dev/null +++ b/iavl/tree_test.go @@ -0,0 +1,1922 @@ +// nolint:errcheck +package iavl + +import ( + "bytes" + "encoding/hex" + "flag" + "fmt" + "math/rand" + "os" + "runtime" + "strconv" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbm "github.com/cosmos/iavl/db" + iavlrand "github.com/cosmos/iavl/internal/rand" +) + +var ( + testLevelDB bool + testFuzzIterations int + random *iavlrand.Rand +) + +func SetupTest() { + random = iavlrand.NewRand() + random.Seed(0) // for determinism + flag.BoolVar(&testLevelDB, "test.leveldb", false, "test leveldb backend") + flag.IntVar(&testFuzzIterations, "test.fuzz-iterations", 100000, "number of fuzz testing iterations") + flag.Parse() +} + +func getTestDB() (dbm.DB, func()) { + if testLevelDB { + d, err := dbm.NewDB("test", "goleveldb", ".") + if err != nil { + panic(err) + } + return d, func() { + d.Close() + os.RemoveAll("./test.db") + } + } + return dbm.NewMemDB(), func() {} +} + +func TestVersionedRandomTree(t *testing.T) { + require := require.New(t) + SetupTest() + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 100, false, log.NewNopLogger()) + versions := 50 + keysPerVersion := 30 + + // Create a tree of size 1000 with 100 versions. + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + k := []byte(iavlrand.RandStr(8)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + } + tree.SaveVersion() + } + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + require.Equal(versions*keysPerVersion, len(leafNodes), "wrong number of nodes") + + // Before deleting old versions, we should have equal or more nodes in the + // db than in the current tree version. + nodes, err := tree.ndb.nodes() + require.Nil(err) + require.True(len(nodes) >= tree.nodeSize()) + + // Ensure it returns all versions in sorted order + available := tree.AvailableVersions() + assert.Equal(t, versions, len(available)) + assert.Equal(t, 1, available[0]) + assert.Equal(t, versions, available[len(available)-1]) + + tree.DeleteVersionsTo(int64(versions - 1)) + + // require.Len(tree.versions, 1, "tree must have one version left") + tr, err := tree.GetImmutable(int64(versions)) + require.NoError(err, "GetImmutable should not error for version %d", versions) + require.Equal(tr.root, tree.root) + + // we should only have one available version now + available = tree.AvailableVersions() + assert.Equal(t, 1, len(available)) + assert.Equal(t, versions, available[0]) + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. + leafNodes, err = tree.ndb.leafNodes() + require.Nil(err) + require.Len(leafNodes, int(tree.Size())) + + nodes, err = tree.ndb.nodes() + require.Nil(err) + require.Equal(tree.nodeSize(), len(nodes)) +} + +// nolint: dupl +func TestTreeHash(t *testing.T) { + const ( + randSeed = 49872768940 // For deterministic tests + keySize = 16 + valueSize = 16 + + versions = 4 // number of versions to generate + versionOps = 4096 // number of operations (create/update/delete) per version + updateRatio = 0.4 // ratio of updates out of all operations + deleteRatio = 0.2 // ratio of deletes out of all operations + ) + + // expected hashes for each version + expectHashes := []string{ + "58ec30fa27f338057e5964ed9ec3367e59b2b54bec4c194f10fde7fed16c2a1c", + "91ad3ace227372f0064b2d63e8493ce8f4bdcbd16c7a8e4f4d54029c9db9570c", + "92c25dce822c5968c228cfe7e686129ea281f79273d4a8fcf6f9130a47aa5421", + "e44d170925554f42e00263155c19574837a38e3efed8910daccc7fa12f560fa0", + } + require.Len(t, expectHashes, versions, "must have expected hashes for all versions") + + r := rand.New(rand.NewSource(randSeed)) + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + keys := make([][]byte, 0, versionOps) + for i := 0; i < versions; i++ { + for j := 0; j < versionOps; j++ { + key := make([]byte, keySize) + value := make([]byte, valueSize) + + // The performance of this is likely to be terrible, but that's fine for small tests + switch { + case len(keys) > 0 && r.Float64() <= deleteRatio: + index := r.Intn(len(keys)) + key = keys[index] + keys = append(keys[:index], keys[index+1:]...) + _, removed, err := tree.Remove(key) + require.NoError(t, err) + require.True(t, removed) + + case len(keys) > 0 && r.Float64() <= updateRatio: + key = keys[r.Intn(len(keys))] + r.Read(value) + updated, err := tree.Set(key, value) + require.NoError(t, err) + require.True(t, updated) + + default: + r.Read(key) + r.Read(value) + // If we get an update, set again + for updated, err := tree.Set(key, value); err == nil && updated; { + key = make([]byte, keySize) + r.Read(key) + } + keys = append(keys, key) + } + } + hash, version, err := tree.SaveVersion() + require.NoError(t, err) + require.EqualValues(t, i+1, version) + require.Equal(t, expectHashes[i], hex.EncodeToString(hash)) + } + + require.EqualValues(t, versions, tree.Version()) +} + +func TestVersionedRandomTreeSmallKeys(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 100, false, log.NewNopLogger()) + singleVersionTree := getTestTree(0) + versions := 20 + keysPerVersion := 50 + + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + k := []byte(iavlrand.RandStr(1)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + singleVersionTree.Set(k, v) + } + tree.SaveVersion() + } + singleVersionTree.SaveVersion() + + for i := 1; i < versions; i++ { + tree.DeleteVersionsTo(int64(i)) + } + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. The simple tree must be equal + // too. + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + + nodes, err := tree.ndb.nodes() + require.Nil(err) + + require.Len(leafNodes, int(tree.Size())) + require.Len(nodes, tree.nodeSize()) + require.Len(nodes, singleVersionTree.nodeSize()) + + // Try getting random keys. + for i := 0; i < keysPerVersion; i++ { + val, err := tree.Get([]byte(iavlrand.RandStr(1))) + require.NoError(err) + require.NotNil(val) + require.NotEmpty(val) + } +} + +func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 100, false, log.NewNopLogger()) + singleVersionTree := getTestTree(0) + versions := 30 + keysPerVersion := 50 + + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + k := []byte(iavlrand.RandStr(1)) + v := []byte(iavlrand.RandStr(8)) + tree.Set(k, v) + singleVersionTree.Set(k, v) + } + tree.SaveVersion() + } + singleVersionTree.SaveVersion() + + for _, i := range iavlrand.RandPerm(versions - 1) { + tree.DeleteVersionsTo(int64(i + 1)) + } + + // After cleaning up all previous versions, we should have as many nodes + // in the db as in the current tree version. The simple tree must be equal + // too. + leafNodes, err := tree.ndb.leafNodes() + require.Nil(err) + + nodes, err := tree.ndb.nodes() + require.Nil(err) + + require.Len(leafNodes, int(tree.Size())) + require.Len(nodes, tree.nodeSize()) + require.Len(nodes, singleVersionTree.nodeSize()) + + // Try getting random keys. + for i := 0; i < keysPerVersion; i++ { + val, err := tree.Get([]byte(iavlrand.RandStr(1))) + require.NoError(err) + require.NotNil(val) + require.NotEmpty(val) + } +} + +func TestVersionedTreeSpecial1(t *testing.T) { + tree := getTestTree(100) + + tree.Set([]byte("C"), []byte("so43QQFN")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("ut7sTTAO")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("AoWWC1kN")) + tree.SaveVersion() + + tree.Set([]byte("T"), []byte("MhkWjkVy")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + tree.DeleteVersionsTo(3) + + nodes, err := tree.ndb.nodes() + require.Nil(t, err) + require.Equal(t, tree.nodeSize(), len(nodes)) +} + +func TestVersionedRandomTreeSpecial2(t *testing.T) { + require := require.New(t) + tree := getTestTree(100) + + tree.Set([]byte("OFMe2Yvm"), []byte("ez2OtQtE")) + tree.Set([]byte("WEN4iN7Y"), []byte("kQNyUalI")) + tree.SaveVersion() + + tree.Set([]byte("1yY3pXHr"), []byte("udYznpII")) + tree.Set([]byte("7OSHNE7k"), []byte("ff181M2d")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Len(nodes, tree.nodeSize()) +} + +func TestVersionedEmptyTree(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + hash, v, err := tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(1, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(2, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(3, v) + + hash, v, err = tree.SaveVersion() + require.NoError(err) + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + require.EqualValues(4, v) + + require.EqualValues(4, tree.Version()) + + require.True(tree.VersionExists(1)) + require.True(tree.VersionExists(3)) + + // Test the empty root loads correctly. + it, err := tree.GetImmutable(3) + require.NoError(err) + require.Nil(it.root) + + require.NoError(tree.DeleteVersionsTo(3)) + + require.False(tree.VersionExists(1)) + require.False(tree.VersionExists(3)) + + tree.Set([]byte("k"), []byte("v")) + + // Now reload the tree. + tree = NewMutableTree(d, 0, false, log.NewNopLogger()) + tree.Load() + + require.False(tree.VersionExists(1)) + require.False(tree.VersionExists(2)) + require.False(tree.VersionExists(3)) + + _, err = tree.GetImmutable(2) + require.Error(err, "GetImmutable should fail for version 2") +} + +func TestVersionedTree(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + // We start with empty database. + require.Equal(0, tree.ndb.size()) + require.True(tree.IsEmpty()) + require.False(tree.IsFastCacheEnabled()) + + // version 0 + + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + + // Still zero keys, since we haven't written them. + nodes, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes, 0) + require.False(tree.IsEmpty()) + + // Now let's write the keys to storage. + hash1, v, err := tree.SaveVersion() + require.NoError(err) + require.False(tree.IsEmpty()) + require.EqualValues(1, v) + + // -----1----- + // key1 = val0 version=1 + // key2 = val0 version=1 + // key2 (root) version=1 + // ----------- + + nodes1, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes1, 2, "db should have a size of 2") + + // version 1 + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + nodes, err = tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes, len(nodes1)) + + hash2, v2, err := tree.SaveVersion() + require.NoError(err) + require.False(bytes.Equal(hash1, hash2)) + require.EqualValues(v+1, v2) + + // Recreate a new tree and load it, to make sure it works in this + // scenario. + tree = NewMutableTree(d, 100, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(err) + + // require.Len(tree.versions, 2, "wrong number of versions") + require.EqualValues(v2, tree.Version()) + + // -----1----- + // key1 = val0 + // key2 = val0 + // -----2----- + // key1 = val1 + // key2 = val1 + // key3 = val1 + // ----------- + + nodes2, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes2, 5, "db should have grown in size") + orphans, err := tree.ndb.orphans() + require.NoError(err) + require.Len(orphans, 3, "db should have three orphans") + + // Create three more orphans. + tree.Remove([]byte("key1")) // orphans both leaf node and inner node containing "key1" and "key2" + tree.Set([]byte("key2"), []byte("val2")) + + hash3, v3, _ := tree.SaveVersion() + require.EqualValues(3, v3) + + // -----1----- + // key1 = val0 (replaced) + // key2 = val0 (replaced) + // -----2----- + // key1 = val1 (removed) + // key2 = val1 (replaced) + // key3 = val1 + // -----3----- + // key2 = val2 + // ----------- + + nodes3, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes3, 6, "wrong number of nodes") + + orphans, err = tree.ndb.orphans() + require.NoError(err) + require.Len(orphans, 7, "wrong number of orphans") + + hash4, _, _ := tree.SaveVersion() + require.EqualValues(hash3, hash4) + require.NotNil(hash4) + + tree = NewMutableTree(d, 100, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(err) + + // ------------ + // DB UNCHANGED + // ------------ + + nodes4, err := tree.ndb.leafNodes() + require.NoError(err) + require.Len(nodes4, len(nodes3), "db should not have changed in size") + + tree.Set([]byte("key1"), []byte("val0")) + + // "key2" + val, err := tree.GetVersioned([]byte("key2"), 0) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Equal("val0", string(val)) + + val, err = tree.GetVersioned([]byte("key2"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.Get([]byte("key2")) + require.NoError(err) + require.Equal("val2", string(val)) + + // "key1" + val, err = tree.GetVersioned([]byte("key1"), 1) + require.NoError(err) + require.Equal("val0", string(val)) + + val, err = tree.GetVersioned([]byte("key1"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.GetVersioned([]byte("key1"), 3) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key1"), 4) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("key1")) + require.NoError(err) + require.Equal("val0", string(val)) + + // "key3" + val, err = tree.GetVersioned([]byte("key3"), 0) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key3"), 2) + require.NoError(err) + require.Equal("val1", string(val)) + + val, err = tree.GetVersioned([]byte("key3"), 3) + require.NoError(err) + require.Equal("val1", string(val)) + + // Delete a version. After this the keys in that version should not be found. + tree.DeleteVersionsTo(2) + + // -----1----- + // key1 = val0 + // key2 = val0 + // -----2----- + // key3 = val1 + // -----3----- + // key2 = val2 + // ----------- + + nodes5, err := tree.ndb.leafNodes() + require.NoError(err) + + require.True(len(nodes5) < len(nodes4), "db should have shrunk after delete %d !< %d", len(nodes5), len(nodes4)) + + val, err = tree.GetVersioned([]byte("key2"), 2) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key3"), 2) + require.NoError(err) + require.Nil(val) + + // But they should still exist in the latest version. + + val, err = tree.Get([]byte("key2")) + require.NoError(err) + require.Equal("val2", string(val)) + + val, err = tree.Get([]byte("key3")) + require.NoError(err) + require.Equal("val1", string(val)) + + // Version 1 should not be available. + + val, err = tree.GetVersioned([]byte("key1"), 1) + require.NoError(err) + require.Nil(val) + + val, err = tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) +} + +func TestVersionedTreeVersionDeletingEfficiency(t *testing.T) { + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + tree.SaveVersion() + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 6) + + tree.Set([]byte("key0"), []byte("val2")) + tree.Remove([]byte("key1")) + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 8) + + tree.DeleteVersionsTo(2) + + leafNodes, err = tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) + + tree2 := getTestTree(0) + tree2.Set([]byte("key0"), []byte("val2")) + tree2.Set([]byte("key2"), []byte("val2")) + tree2.Set([]byte("key3"), []byte("val1")) + tree2.SaveVersion() + + require.Equal(t, tree2.nodeSize(), tree.nodeSize()) +} + +func TestVersionedTreeOrphanDeleting(t *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("key0"), []byte("val0")) + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.Set([]byte("key3"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key0"), []byte("val2")) + tree.Remove([]byte("key1")) + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + val, err := tree.Get([]byte("key0")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key1")) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.Get([]byte("key2")) + require.NoError(t, err) + require.Equal(t, val, []byte("val2")) + + val, err = tree.Get([]byte("key3")) + require.NoError(t, err) + require.Equal(t, val, []byte("val1")) + + tree.DeleteVersionsTo(1) + + leafNodes, err := tree.ndb.leafNodes() + require.Nil(t, err) + require.Len(t, leafNodes, 3) +} + +func TestVersionedTreeSpecialCase(t *testing.T) { + require := require.New(t) + d, closeDB := getTestDB() + defer closeDB() + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + val, err := tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) +} + +func TestVersionedTreeSpecialCase2(t *testing.T) { + require := require.New(t) + + d := dbm.NewMemDB() + tree := NewMutableTree(d, 100, false, log.NewNopLogger()) + + tree.Set([]byte("key1"), []byte("val0")) + tree.Set([]byte("key2"), []byte("val0")) + tree.SaveVersion() + + tree.Set([]byte("key1"), []byte("val1")) + tree.Set([]byte("key2"), []byte("val1")) + tree.SaveVersion() + + tree.Set([]byte("key2"), []byte("val2")) + tree.SaveVersion() + + tree = NewMutableTree(d, 100, false, log.NewNopLogger()) + _, err := tree.Load() + require.NoError(err) + + require.NoError(tree.DeleteVersionsTo(2)) + + val, err := tree.GetVersioned([]byte("key2"), 1) + require.NoError(err) + require.Nil(val) +} + +func TestVersionedTreeSpecialCase3(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + + tree.Set([]byte("m"), []byte("liWT0U6G")) + tree.Set([]byte("G"), []byte("7PxRXwUA")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("XRLXgf8C")) + tree.SaveVersion() + + tree.Set([]byte("r"), []byte("bBEmIXBU")) + tree.SaveVersion() + + tree.Set([]byte("i"), []byte("kkIS35te")) + tree.SaveVersion() + + tree.Set([]byte("k"), []byte("CpEnpzKJ")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + tree.DeleteVersionsTo(3) + tree.DeleteVersionsTo(4) + + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Equal(tree.nodeSize(), len(nodes)) +} + +func TestVersionedTreeSaveAndLoad(t *testing.T) { + require := require.New(t) + d := dbm.NewMemDB() + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + + // Loading with an empty root is a no-op. + tree.Load() + + tree.Set([]byte("C"), []byte("so43QQFN")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("ut7sTTAO")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("AoWWC1kN")) + tree.SaveVersion() + + tree.SaveVersion() + tree.SaveVersion() + tree.SaveVersion() + + preHash := tree.Hash() + require.NotNil(preHash) + + require.Equal(int64(6), tree.Version()) + + // Reload the tree, to test that roots and orphans are properly loaded. + ntree := NewMutableTree(d, 0, false, log.NewNopLogger()) + ntree.Load() + + require.False(ntree.IsEmpty()) + require.Equal(int64(6), ntree.Version()) + + postHash := ntree.Hash() + require.Equal(preHash, postHash) + + ntree.Set([]byte("T"), []byte("MhkWjkVy")) + ntree.SaveVersion() + + ntree.DeleteVersionsTo(6) + + require.False(ntree.IsEmpty()) + require.Equal(int64(4), ntree.Size()) + nodes, err := tree.ndb.nodes() + require.NoError(err) + require.Len(nodes, ntree.nodeSize()) +} + +func TestVersionedTreeErrors(t *testing.T) { + require := require.New(t) + tree := getTestTree(100) + + // Can't delete non-existent versions. + require.Error(tree.DeleteVersionsTo(1)) + require.Error(tree.DeleteVersionsTo(99)) + + tree.Set([]byte("key"), []byte("val")) + + // Saving with content is ok. + _, _, err := tree.SaveVersion() + require.NoError(err) + + // Can't delete current version. + require.Error(tree.DeleteVersionsTo(1)) + + // Trying to get a key from a version which doesn't exist. + val, err := tree.GetVersioned([]byte("key"), 404) + require.NoError(err) + require.Nil(val) + + // Same thing with proof. We get an error because a proof couldn't be + // constructed. + _, err = tree.GetVersionedProof([]byte("key"), 404) + require.Error(err) +} + +func TestVersionedCheckpointsSpecialCase(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + key := []byte("k") + + tree.Set(key, []byte("val1")) + + tree.SaveVersion() + // ... + tree.SaveVersion() + // ... + tree.SaveVersion() + // ... + // This orphans "k" at version 1. + tree.Set(key, []byte("val2")) + tree.SaveVersion() + + // When version 1 is deleted, the orphans should move to the next + // checkpoint, which is version 10. + tree.DeleteVersionsTo(1) + + val, err := tree.GetVersioned(key, 2) + require.Nil(err) + require.NotEmpty(val) + require.Equal([]byte("val1"), val) +} + +func TestVersionedCheckpointsSpecialCase2(_ *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("U"), []byte("XamDUtiJ")) + tree.Set([]byte("A"), []byte("UkZBuYIU")) + tree.Set([]byte("H"), []byte("7a9En4uw")) + tree.Set([]byte("V"), []byte("5HXU3pSI")) + tree.SaveVersion() + + tree.Set([]byte("U"), []byte("Replaced")) + tree.Set([]byte("A"), []byte("Replaced")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("New")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) +} + +func TestVersionedCheckpointsSpecialCase3(_ *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("n"), []byte("2wUCUs8q")) + tree.Set([]byte("l"), []byte("WQ7mvMbc")) + tree.SaveVersion() + + tree.Set([]byte("N"), []byte("ved29IqU")) + tree.Set([]byte("v"), []byte("01jquVXU")) + tree.SaveVersion() + + tree.Set([]byte("l"), []byte("bhIpltPM")) + tree.Set([]byte("B"), []byte("rj97IKZh")) + tree.SaveVersion() + + tree.DeleteVersionsTo(2) + + tree.GetVersioned([]byte("m"), 1) +} + +func TestVersionedCheckpointsSpecialCase4(t *testing.T) { + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + tree.Set([]byte("U"), []byte("XamDUtiJ")) + tree.Set([]byte("A"), []byte("UkZBuYIU")) + tree.Set([]byte("H"), []byte("7a9En4uw")) + tree.Set([]byte("V"), []byte("5HXU3pSI")) + tree.SaveVersion() + + tree.Remove([]byte("U")) + tree.Remove([]byte("A")) + tree.SaveVersion() + + tree.Set([]byte("X"), []byte("New")) + tree.SaveVersion() + + val, err := tree.GetVersioned([]byte("A"), 2) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.GetVersioned([]byte("A"), 1) + require.NoError(t, err) + require.NotEmpty(t, val) + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + + val, err = tree.GetVersioned([]byte("A"), 2) + require.NoError(t, err) + require.Nil(t, val) + + val, err = tree.GetVersioned([]byte("A"), 1) + require.NoError(t, err) + require.Nil(t, val) +} + +func TestVersionedCheckpointsSpecialCase5(_ *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("R"), []byte("ygZlIzeW")) + tree.SaveVersion() + + tree.Set([]byte("j"), []byte("ZgmCWyo2")) + tree.SaveVersion() + + tree.Set([]byte("R"), []byte("vQDaoz6Z")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + + tree.GetVersioned([]byte("R"), 2) +} + +func TestVersionedCheckpointsSpecialCase6(_ *testing.T) { + tree := getTestTree(0) + + tree.Set([]byte("Y"), []byte("MW79JQeV")) + tree.Set([]byte("7"), []byte("Kp0ToUJB")) + tree.Set([]byte("Z"), []byte("I26B1jPG")) + tree.Set([]byte("6"), []byte("ZG0iXq3h")) + tree.Set([]byte("2"), []byte("WOR27LdW")) + tree.Set([]byte("4"), []byte("MKMvc6cn")) + tree.SaveVersion() + + tree.Set([]byte("1"), []byte("208dOu40")) + tree.Set([]byte("G"), []byte("7isI9OQH")) + tree.Set([]byte("8"), []byte("zMC1YwpH")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("bn62vWbq")) + tree.Set([]byte("5"), []byte("wZuLGDkZ")) + tree.SaveVersion() + + tree.DeleteVersionsTo(1) + tree.DeleteVersionsTo(2) + + tree.GetVersioned([]byte("Y"), 1) + tree.GetVersioned([]byte("7"), 1) + tree.GetVersioned([]byte("Z"), 1) + tree.GetVersioned([]byte("6"), 1) + tree.GetVersioned([]byte("s"), 1) + tree.GetVersioned([]byte("2"), 1) + tree.GetVersioned([]byte("4"), 1) +} + +func TestVersionedCheckpointsSpecialCase7(_ *testing.T) { + tree := getTestTree(100) + + tree.Set([]byte("n"), []byte("OtqD3nyn")) + tree.Set([]byte("W"), []byte("kMdhJjF5")) + tree.Set([]byte("A"), []byte("BM3BnrIb")) + tree.Set([]byte("I"), []byte("QvtCH970")) + tree.Set([]byte("L"), []byte("txKgOTqD")) + tree.Set([]byte("Y"), []byte("NAl7PC5L")) + tree.SaveVersion() + + tree.Set([]byte("7"), []byte("qWcEAlyX")) + tree.SaveVersion() + + tree.Set([]byte("M"), []byte("HdQwzA64")) + tree.Set([]byte("3"), []byte("2Naa77fo")) + tree.Set([]byte("A"), []byte("SRuwKOTm")) + tree.Set([]byte("I"), []byte("oMX4aAOy")) + tree.Set([]byte("4"), []byte("dKfvbEOc")) + tree.SaveVersion() + + tree.Set([]byte("D"), []byte("3U4QbXCC")) + tree.Set([]byte("B"), []byte("FxExhiDq")) + tree.SaveVersion() + + tree.Set([]byte("A"), []byte("tWQgbFCY")) + tree.SaveVersion() + + tree.DeleteVersionsTo(4) + + tree.GetVersioned([]byte("A"), 3) +} + +func TestVersionedTreeEfficiency(t *testing.T) { + require := require.New(t) + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + versions := 20 + keysPerVersion := 100 + keysAddedPerVersion := map[int]int{} + + keysAdded := 0 + for i := 1; i <= versions; i++ { + for j := 0; j < keysPerVersion; j++ { + // Keys of size one are likely to be overwritten. + tree.Set([]byte(iavlrand.RandStr(1)), []byte(iavlrand.RandStr(8))) + } + nodes, err := tree.ndb.nodes() + require.NoError(err) + sizeBefore := len(nodes) + tree.SaveVersion() + _, err = tree.ndb.nodes() + require.NoError(err) + nodes, err = tree.ndb.nodes() + require.NoError(err) + sizeAfter := len(nodes) + change := sizeAfter - sizeBefore + keysAddedPerVersion[i] = change + keysAdded += change + } + + keysDeleted := 0 + for i := 1; i < versions; i++ { + if tree.VersionExists(int64(i)) { + nodes, err := tree.ndb.nodes() + require.NoError(err) + sizeBefore := len(nodes) + tree.DeleteVersionsTo(int64(i)) + nodes, err = tree.ndb.nodes() + require.NoError(err) + sizeAfter := len(nodes) + + change := sizeBefore - sizeAfter + keysDeleted += change + + require.InDelta(change, keysAddedPerVersion[i], float64(keysPerVersion)/5) + } + } + require.Equal(keysAdded-tree.nodeSize(), keysDeleted) +} + +func TestVersionedTreeProofs(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + + tree.Set([]byte("k1"), []byte("v1")) + tree.Set([]byte("k2"), []byte("v1")) + tree.Set([]byte("k3"), []byte("v1")) + _, _, err := tree.SaveVersion() + require.NoError(err) + + // fmt.Println("TREE VERSION 1") + // printNode(tree.ndb, tree.root, 0) + // fmt.Println("TREE VERSION 1 END") + + root1 := tree.Hash() + + tree.Set([]byte("k2"), []byte("v2")) + tree.Set([]byte("k4"), []byte("v2")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + // fmt.Println("TREE VERSION 2") + // printNode(tree.ndb, tree.root, 0) + // fmt.Println("TREE VERSION END") + + root2 := tree.Hash() + require.NotEqual(root1, root2) + + tree.Remove([]byte("k2")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + root3 := tree.Hash() + require.NotEqual(root2, root3) + + iTree, err := tree.GetImmutable(1) + require.NoError(err) + + proof, err := tree.GetVersionedProof([]byte("k2"), 1) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v1")) + res, err := iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) + + proof, err = tree.GetVersionedProof([]byte("k4"), 1) + require.NoError(err) + require.EqualValues(proof.GetNonexist().Key, []byte("k4")) + res, err = iTree.VerifyProof(proof, []byte("k4")) + require.NoError(err) + require.True(res) + + iTree, err = tree.GetImmutable(2) + require.NoError(err) + proof, err = tree.GetVersionedProof([]byte("k2"), 2) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v2")) + res, err = iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) + + proof, err = tree.GetVersionedProof([]byte("k1"), 2) + require.NoError(err) + require.EqualValues(proof.GetExist().Value, []byte("v1")) + res, err = iTree.VerifyProof(proof, []byte("k1")) + require.NoError(err) + require.True(res) + + iTree, err = tree.GetImmutable(3) + require.NoError(err) + proof, err = tree.GetVersionedProof([]byte("k2"), 3) + require.NoError(err) + require.EqualValues(proof.GetNonexist().Key, []byte("k2")) + res, err = iTree.VerifyProof(proof, []byte("k2")) + require.NoError(err) + require.True(res) +} + +func TestOrphans(t *testing.T) { + // If you create a sequence of saved versions + // Then randomly delete versions other than the first and last until only those two remain + // Any remaining orphan nodes should either have fromVersion == firstVersion || toVersion == lastVersion + require := require.New(t) + tree := NewMutableTree(dbm.NewMemDB(), 100, false, log.NewNopLogger()) + + NUMVERSIONS := 100 + NUMUPDATES := 100 + + for i := 0; i < NUMVERSIONS; i++ { + for j := 1; j < NUMUPDATES; j++ { + tree.Set(iavlrand.RandBytes(2), iavlrand.RandBytes(2)) + } + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not error") + } + + for v := 1; v < NUMVERSIONS; v++ { + err := tree.DeleteVersionsTo(int64(v)) + require.NoError(err, "DeleteVersion should not error") + } +} + +func TestVersionedTreeHash(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + + hash := tree.Hash() + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + tree.Set([]byte("I"), []byte("D")) + hash = tree.Hash() + require.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hex.EncodeToString(hash)) + + hash1, _, err := tree.SaveVersion() + require.NoError(err) + + tree.Set([]byte("I"), []byte("F")) + hash = tree.Hash() + require.EqualValues(hash1, hash) + + _, _, err = tree.SaveVersion() + require.NoError(err) + + proof, err := tree.GetVersionedProof([]byte("I"), 2) + require.NoError(err) + require.EqualValues([]byte("F"), proof.GetExist().Value) + iTree, err := tree.GetImmutable(2) + require.NoError(err) + res, err := iTree.VerifyProof(proof, []byte("I")) + require.NoError(err) + require.True(res) +} + +func TestNilValueSemantics(t *testing.T) { + require := require.New(t) + tree := getTestTree(0) + + _, err := tree.Set([]byte("k"), nil) + require.Error(err) +} + +func TestCopyValueSemantics(t *testing.T) { + require := require.New(t) + + tree := getTestTree(0) + + val := []byte("v1") + + tree.Set([]byte("k"), val) + v, err := tree.Get([]byte("k")) + require.NoError(err) + require.Equal([]byte("v1"), v) + + val[1] = '2' + + val, err = tree.Get([]byte("k")) + require.NoError(err) + require.Equal([]byte("v2"), val) +} + +func TestRollback(t *testing.T) { + require := require.New(t) + + tree := getTestTree(0) + + tree.Set([]byte("k"), []byte("v")) + tree.SaveVersion() + + tree.Set([]byte("r"), []byte("v")) + tree.Set([]byte("s"), []byte("v")) + + tree.Rollback() + + tree.Set([]byte("t"), []byte("v")) + + tree.SaveVersion() + + require.Equal(int64(2), tree.Size()) + + val, err := tree.Get([]byte("r")) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("s")) + require.NoError(err) + require.Nil(val) + + val, err = tree.Get([]byte("t")) + require.NoError(err) + require.Equal([]byte("v"), val) +} + +func TestLoadVersion(t *testing.T) { + tree := getTestTree(0) + maxVersions := 10 + + version, err := tree.LoadVersion(0) + require.NoError(t, err, "unexpected error") + require.Equal(t, version, int64(0), "expected latest version to be zero") + + for i := 0; i < maxVersions; i++ { + tree.Set([]byte(fmt.Sprintf("key_%d", i+1)), []byte(fmt.Sprintf("value_%d", i+1))) + + _, _, err = tree.SaveVersion() + require.NoError(t, err, "SaveVersion should not fail") + } + + // require the ability to load the latest version + version, err = tree.LoadVersion(int64(maxVersions)) + require.NoError(t, err, "unexpected error when lazy loading version") + require.Equal(t, version, int64(maxVersions)) + + value, err := tree.Get([]byte(fmt.Sprintf("key_%d", maxVersions))) + require.NoError(t, err) + require.Equal(t, value, []byte(fmt.Sprintf("value_%d", maxVersions)), "unexpected value") + + // require the ability to load an older version + version, err = tree.LoadVersion(int64(maxVersions - 1)) + require.NoError(t, err, "unexpected error when loading version") + require.Equal(t, version, int64(maxVersions)) + + value, err = tree.Get([]byte(fmt.Sprintf("key_%d", maxVersions-1))) + require.NoError(t, err) + require.Equal(t, value, []byte(fmt.Sprintf("value_%d", maxVersions-1)), "unexpected value") + + // require the inability to load a non-valid version + version, err = tree.LoadVersion(int64(maxVersions + 1)) + require.Error(t, err, "expected error when loading version") + require.Equal(t, version, int64(maxVersions)) +} + +func TestOverwrite(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 0, false, log.NewNopLogger()) + + // Set one kv pair and save version 1 + tree.Set([]byte("key1"), []byte("value1")) + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + // Set another kv pair and save version 2 + tree.Set([]byte("key2"), []byte("value2")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + // Reload tree at version 1 + tree = NewMutableTree(mdb, 0, false, log.NewNopLogger()) + _, err = tree.LoadVersion(int64(1)) + require.NoError(err, "LoadVersion should not fail") + + // Attempt to put a different kv pair into the tree and save + tree.Set([]byte("key2"), []byte("different value 2")) + _, _, err = tree.SaveVersion() + require.Error(err, "SaveVersion should fail because of changed value") + + // Replay the original transition from version 1 to version 2 and attempt to save + tree.Set([]byte("key2"), []byte("value2")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was idempotent") +} + +func TestOverwriteEmpty(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 0, false, log.NewNopLogger()) + + // Save empty version 1 + _, _, err := tree.SaveVersion() + require.NoError(err) + + // Save empty version 2 + _, _, err = tree.SaveVersion() + require.NoError(err) + + // Save a key in version 3 + tree.Set([]byte("key"), []byte("value")) + _, _, err = tree.SaveVersion() + require.NoError(err) + + // Load version 1 and attempt to save a different key + _, err = tree.LoadVersion(1) + require.NoError(err) + tree.Set([]byte("foo"), []byte("bar")) + _, _, err = tree.SaveVersion() + require.Error(err) + + // However, deleting the key and saving an empty version should work, + // since it's the same as the existing version. + tree.Remove([]byte("foo")) + _, version, err := tree.SaveVersion() + require.NoError(err) + require.EqualValues(2, version) +} + +func TestLoadVersionForOverwriting(t *testing.T) { + require := require.New(t) + + mdb := dbm.NewMemDB() + tree := NewMutableTree(mdb, 0, false, log.NewNopLogger()) + + maxLength := 100 + for count := 1; count <= maxLength; count++ { + countStr := strconv.Itoa(count) + // Set one kv pair and save version + tree.Set([]byte("key"+countStr), []byte("value"+countStr)) + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + } + + tree = NewMutableTree(mdb, 0, false, log.NewNopLogger()) + require.Error(tree.LoadVersionForOverwriting(int64(maxLength * 2))) + + tree = NewMutableTree(mdb, 0, false, log.NewNopLogger()) + err := tree.LoadVersionForOverwriting(int64(maxLength / 2)) + require.NoError(err, "LoadVersion should not fail") + + for version := 1; version <= maxLength/2; version++ { + exist := tree.VersionExists(int64(version)) + require.True(exist, "versions no more than 50 should exist") + } + + for version := (maxLength / 2) + 1; version <= maxLength; version++ { + exist := tree.VersionExists(int64(version)) + require.False(exist, "versions more than 50 should have been deleted") + } + + tree.Set([]byte("key49"), []byte("value49 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was allowed") + + tree.Set([]byte("key50"), []byte("value50 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, overwrite was allowed") + + // Reload tree at version 50, the latest tree version is 52 + tree = NewMutableTree(mdb, 0, false, log.NewNopLogger()) + _, err = tree.LoadVersion(int64(maxLength / 2)) + require.NoError(err, "LoadVersion should not fail") + + tree.Set([]byte("key49"), []byte("value49 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, write the same value") + + tree.Set([]byte("key50"), []byte("value50 different different")) + _, _, err = tree.SaveVersion() + require.Error(err, "SaveVersion should fail, overwrite was not allowed") + + tree.Set([]byte("key50"), []byte("value50 different")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail, write the same value") + + // The tree version now is 52 which is equal to latest version. + // Now any key value can be written into the tree + tree.Set([]byte("key any value"), []byte("value any value")) + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail.") +} + +// BENCHMARKS + +func BenchmarkTreeLoadAndDelete(b *testing.B) { + numVersions := 5000 + numKeysPerVersion := 10 + + d, err := dbm.NewDB("bench", "goleveldb", ".") + if err != nil { + panic(err) + } + defer d.Close() + defer os.RemoveAll("./bench.db") + + tree := NewMutableTree(d, 0, false, log.NewNopLogger()) + for v := 1; v < numVersions; v++ { + for i := 0; i < numKeysPerVersion; i++ { + tree.Set([]byte(iavlrand.RandStr(16)), iavlrand.RandBytes(32)) + } + tree.SaveVersion() + } + + b.Run("LoadAndDelete", func(b *testing.B) { + for n := 0; n < b.N; n++ { + b.StopTimer() + tree = NewMutableTree(d, 0, false, log.NewNopLogger()) + require.NoError(b, err) + runtime.GC() + b.StartTimer() + + // Load the tree from disk. + tree.Load() + + // Delete about 10% of the versions randomly. + // The trade-off is usually between load efficiency and delete + // efficiency, which is why we do both in this benchmark. + // If we can load quickly into a data-structure that allows for + // efficient deletes, we are golden. + for v := 0; v < numVersions/10; v++ { + version := (iavlrand.RandInt() % numVersions) + 1 + tree.DeleteVersionsTo(int64(version)) + } + } + }) +} + +func TestLoadVersionForOverwritingCase2(t *testing.T) { + require := require.New(t) + + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i}) + } + + _, _, err := tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 1}) + } + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail with the same key") + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 2}) + } + tree.SaveVersion() + + removedNodes := []*Node{} + + nodes, err := tree.ndb.nodes() + require.NoError(err) + for _, n := range nodes { + if n.nodeKey.version > 1 { + removedNodes = append(removedNodes, n) + } + } + + err = tree.LoadVersionForOverwriting(1) + require.NoError(err, "LoadVersionForOverwriting should not fail") + + for i := byte(0); i < 20; i++ { + v, err := tree.Get([]byte{i}) + require.NoError(err) + require.Equal([]byte{i}, v) + } + + for _, n := range removedNodes { + has, _ := tree.ndb.Has(n.GetKey()) + require.False(has, "LoadVersionForOverwriting should remove useless nodes") + } + + tree.Set([]byte{0x2}, []byte{0x3}) + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") + + err = tree.DeleteVersionsTo(1) + require.NoError(err, "DeleteVersion should not fail") + + tree.Set([]byte{0x1}, []byte{0x3}) + + _, _, err = tree.SaveVersion() + require.NoError(err, "SaveVersion should not fail") +} + +func TestLoadVersionForOverwritingCase3(t *testing.T) { + require := require.New(t) + + tree := NewMutableTree(dbm.NewMemDB(), 0, false, log.NewNopLogger()) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i}) + } + _, _, err := tree.SaveVersion() + require.NoError(err) + + for i := byte(0); i < 20; i++ { + tree.Set([]byte{i}, []byte{i + 1}) + } + _, _, err = tree.SaveVersion() + require.NoError(err) + + removedNodes := []*Node{} + + nodes, err := tree.ndb.nodes() + require.NoError(err) + for _, n := range nodes { + if n.nodeKey.version > 1 { + removedNodes = append(removedNodes, n) + } + } + + for i := byte(0); i < 20; i++ { + tree.Remove([]byte{i}) + } + _, _, err = tree.SaveVersion() + require.NoError(err) + + err = tree.LoadVersionForOverwriting(1) + require.NoError(err) + for _, n := range removedNodes { + has, err := tree.ndb.Has(n.GetKey()) + require.NoError(err) + require.False(has, "LoadVersionForOverwriting should remove useless nodes") + } + + for i := byte(0); i < 20; i++ { + v, err := tree.Get([]byte{i}) + require.NoError(err) + require.Equal([]byte{i}, v) + } +} + +func TestIterate_ImmutableTree_Version1(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + assertImmutableMirrorIterate(t, immutableTree, mirror) +} + +func TestIterate_ImmutableTree_Version2(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + randomizeTreeAndMirror(t, tree, mirror) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(2) + require.NoError(t, err) + + assertImmutableMirrorIterate(t, immutableTree, mirror) +} + +func TestGetByIndex_ImmutableTree(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + mirrorKeys := getSortedMirrorKeys(mirror) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + isFastCacheEnabled, err := immutableTree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + for index, expectedKey := range mirrorKeys { + expectedValue := mirror[expectedKey] + + actualKey, actualValue, err := immutableTree.GetByIndex(int64(index)) + require.NoError(t, err) + + require.Equal(t, expectedKey, string(actualKey)) + require.Equal(t, expectedValue, string(actualValue)) + } +} + +func TestGetWithIndex_ImmutableTree(t *testing.T) { + tree, mirror := getRandomizedTreeAndMirror(t) + mirrorKeys := getSortedMirrorKeys(mirror) + + _, _, err := tree.SaveVersion() + require.NoError(t, err) + + immutableTree, err := tree.GetImmutable(1) + require.NoError(t, err) + + isFastCacheEnabled, err := immutableTree.IsFastCacheEnabled() + require.NoError(t, err) + require.True(t, isFastCacheEnabled) + + for expectedIndex, key := range mirrorKeys { + expectedValue := mirror[key] + + actualIndex, actualValue, err := immutableTree.GetWithIndex([]byte(key)) + require.NoError(t, err) + + require.Equal(t, expectedValue, string(actualValue)) + require.Equal(t, int64(expectedIndex), actualIndex) + } +} + +func Benchmark_GetWithIndex(b *testing.B) { + db, err := dbm.NewDB("test", "memdb", "") + require.NoError(b, err) + + const numKeyVals = 100000 + + t := NewMutableTree(db, numKeyVals, false, log.NewNopLogger()) + + keys := make([][]byte, 0, numKeyVals) + + for i := 0; i < numKeyVals; i++ { + key := iavlrand.RandBytes(10) + keys = append(keys, key) + t.Set(key, iavlrand.RandBytes(10)) + } + _, _, err = t.SaveVersion() + require.NoError(b, err) + + b.ReportAllocs() + runtime.GC() + + b.Run("fast", func(sub *testing.B) { + isFastCacheEnabled, err := t.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + b.ResetTimer() + for i := 0; i < sub.N; i++ { + randKey := rand.Intn(numKeyVals) + t.GetWithIndex(keys[randKey]) + } + }) + + b.Run("regular", func(sub *testing.B) { + // get non-latest version to force regular storage + _, latestVersion, err := t.SaveVersion() + require.NoError(b, err) + + itree, err := t.GetImmutable(latestVersion - 1) + require.NoError(b, err) + + isFastCacheEnabled, err := itree.IsFastCacheEnabled() + require.NoError(b, err) + require.False(b, isFastCacheEnabled) + b.ResetTimer() + for i := 0; i < sub.N; i++ { + randKey := rand.Intn(numKeyVals) + itree.GetWithIndex(keys[randKey]) + } + }) +} + +func Benchmark_GetByIndex(b *testing.B) { + db, err := dbm.NewDB("test", "memdb", "") + require.NoError(b, err) + + const numKeyVals = 100000 + + t := NewMutableTree(db, numKeyVals, false, log.NewNopLogger()) + + for i := 0; i < numKeyVals; i++ { + key := iavlrand.RandBytes(10) + t.Set(key, iavlrand.RandBytes(10)) + } + _, _, err = t.SaveVersion() + require.NoError(b, err) + + b.ReportAllocs() + runtime.GC() + + b.Run("fast", func(sub *testing.B) { + isFastCacheEnabled, err := t.IsFastCacheEnabled() + require.NoError(b, err) + require.True(b, isFastCacheEnabled) + b.ResetTimer() + for i := 0; i < sub.N; i++ { + randIdx := rand.Intn(numKeyVals) + t.GetByIndex(int64(randIdx)) + } + }) + + b.Run("regular", func(sub *testing.B) { + // get non-latest version to force regular storage + _, latestVersion, err := t.SaveVersion() + require.NoError(b, err) + + itree, err := t.GetImmutable(latestVersion - 1) + require.NoError(b, err) + + isFastCacheEnabled, err := itree.IsFastCacheEnabled() + require.NoError(b, err) + require.False(b, isFastCacheEnabled) + + b.ResetTimer() + for i := 0; i < sub.N; i++ { + randIdx := rand.Intn(numKeyVals) + itree.GetByIndex(int64(randIdx)) + } + }) +} + +func TestNodeCacheStatisic(t *testing.T) { + const numKeyVals = 100000 + testcases := map[string]struct { + cacheSize int + expectFastCacheHitCnt int + expectFastCacheMissCnt int + expectCacheHitCnt int + expectCacheMissCnt int + }{ + "with_cache": { + cacheSize: numKeyVals, + expectFastCacheHitCnt: numKeyVals, + expectFastCacheMissCnt: 0, + expectCacheHitCnt: 1, + expectCacheMissCnt: 0, + }, + "without_cache": { + cacheSize: 0, + expectFastCacheHitCnt: 100000, // this value is hardcoded in nodedb for fast cache. + expectFastCacheMissCnt: 0, + expectCacheHitCnt: 0, + expectCacheMissCnt: 1, + }, + } + + for name, tc := range testcases { + tc := tc + t.Run(name, func(sub *testing.T) { + stat := &Statistics{} + db, err := dbm.NewDB("test", "memdb", "") + require.NoError(t, err) + mt := NewMutableTree(db, tc.cacheSize, false, log.NewNopLogger(), StatOption(stat)) + + for i := 0; i < numKeyVals; i++ { + key := []byte(strconv.Itoa(i)) + _, err := mt.Set(key, iavlrand.RandBytes(10)) + require.NoError(t, err) + } + _, ver, _ := mt.SaveVersion() + it, err := mt.GetImmutable(ver) + require.NoError(t, err) + + for i := 0; i < numKeyVals; i++ { + key := []byte(strconv.Itoa(i)) + val, err := it.Get(key) + require.NoError(t, err) + require.NotNil(t, val) + require.NotEmpty(t, val) + } + require.Equal(t, tc.expectFastCacheHitCnt, int(stat.GetFastCacheHitCnt())) + require.Equal(t, tc.expectFastCacheMissCnt, int(stat.GetFastCacheMissCnt())) + require.Equal(t, tc.expectCacheHitCnt, int(stat.GetCacheHitCnt())) + require.Equal(t, tc.expectCacheMissCnt, int(stat.GetCacheMissCnt())) + }) + } +} + +func TestEmptyVersionDelete(t *testing.T) { + db, err := dbm.NewDB("test", "memdb", "") + require.NoError(t, err) + defer db.Close() + + tree := NewMutableTree(db, 0, false, log.NewNopLogger()) + + _, err = tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + toVersion := 10 + for i := 0; i < toVersion; i++ { + _, _, err = tree.SaveVersion() + require.NoError(t, err) + } + + require.NoError(t, tree.DeleteVersionsTo(5)) + + // Load the tree from disk. + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + v, err := tree.Load() + require.NoError(t, err) + require.Equal(t, int64(toVersion), v) + // Version 1 is only meaningful, so it should not be deleted. + require.Equal(t, tree.root.GetKey(), (&NodeKey{version: 1, nonce: 0}).GetKey()) + // it is expected that the version 1 is deleted. + versions := tree.AvailableVersions() + require.Equal(t, 6, versions[0]) + require.Len(t, versions, 5) +} + +func TestReferenceRoot(t *testing.T) { + db, err := dbm.NewDB("test", "memdb", "") + require.NoError(t, err) + defer db.Close() + + tree := NewMutableTree(db, 0, false, log.NewNopLogger()) + + _, err = tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + _, err = tree.Set([]byte("key2"), []byte("value2")) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, _, err = tree.Remove([]byte("key1")) + require.NoError(t, err) + + // the root will be the leaf node of key2 + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + // load the tree from disk + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + require.Equal(t, int64(2), tree.Version()) + // check the root of version 2 is the leaf node of key2 + require.Equal(t, tree.root.GetKey(), (&NodeKey{version: 1, nonce: 3}).GetKey()) + require.Equal(t, tree.root.key, []byte("key2")) + + // test the reference root when pruning + db, err = dbm.NewDB("test", "memdb", "") + require.NoError(t, err) + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + + _, err = tree.Set([]byte("key1"), []byte("value1")) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + _, _, err = tree.SaveVersion() // empty version + require.NoError(t, err) + + require.NoError(t, tree.DeleteVersionsTo(1)) + _, _, err = tree.SaveVersion() // empty version + require.NoError(t, err) + + // load the tree from disk + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + + _, err = tree.Set([]byte("key2"), []byte("value2")) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + + // load the tree from disk to check if the reference root is loaded correctly + tree = NewMutableTree(db, 0, false, log.NewNopLogger()) + _, err = tree.Load() + require.NoError(t, err) + _, err = tree.Set([]byte("key1"), []byte("value2")) + require.NoError(t, err) +} diff --git a/iavl/unsaved_fast_iterator.go b/iavl/unsaved_fast_iterator.go new file mode 100644 index 000000000..eeaff812c --- /dev/null +++ b/iavl/unsaved_fast_iterator.go @@ -0,0 +1,229 @@ +package iavl + +import ( + "bytes" + "errors" + "sort" + "sync" + + dbm "github.com/cosmos/cosmos-db" + + "github.com/cosmos/iavl/fastnode" + ibytes "github.com/cosmos/iavl/internal/bytes" +) + +var ( + errUnsavedFastIteratorNilAdditionsGiven = errors.New("unsaved fast iterator must be created with unsaved additions but they were nil") + + errUnsavedFastIteratorNilRemovalsGiven = errors.New("unsaved fast iterator must be created with unsaved removals but they were nil") +) + +// UnsavedFastIterator is a dbm.Iterator for ImmutableTree +// it iterates over the latest state via fast nodes, +// taking advantage of keys being located in sequence in the underlying database. +type UnsavedFastIterator struct { + start, end []byte + valid bool + ascending bool + err error + ndb *nodeDB + nextKey []byte + nextVal []byte + fastIterator dbm.Iterator + + nextUnsavedNodeIdx int + unsavedFastNodeAdditions *sync.Map // map[string]*FastNode + unsavedFastNodeRemovals *sync.Map // map[string]interface{} + unsavedFastNodesToSort []string +} + +var _ dbm.Iterator = (*UnsavedFastIterator)(nil) + +func NewUnsavedFastIterator(start, end []byte, ascending bool, ndb *nodeDB, unsavedFastNodeAdditions, unsavedFastNodeRemovals *sync.Map) *UnsavedFastIterator { + iter := &UnsavedFastIterator{ + start: start, + end: end, + ascending: ascending, + ndb: ndb, + unsavedFastNodeAdditions: unsavedFastNodeAdditions, + unsavedFastNodeRemovals: unsavedFastNodeRemovals, + nextKey: nil, + nextVal: nil, + nextUnsavedNodeIdx: 0, + fastIterator: NewFastIterator(start, end, ascending, ndb), + } + + if iter.ndb == nil { + iter.err = errFastIteratorNilNdbGiven + iter.valid = false + return iter + } + + if iter.unsavedFastNodeAdditions == nil { + iter.err = errUnsavedFastIteratorNilAdditionsGiven + iter.valid = false + return iter + } + + if iter.unsavedFastNodeRemovals == nil { + iter.err = errUnsavedFastIteratorNilRemovalsGiven + iter.valid = false + return iter + } + // We need to ensure that we iterate over saved and unsaved state in order. + // The strategy is to sort unsaved nodes, the fast node on disk are already sorted. + // Then, we keep a pointer to both the unsaved and saved nodes, and iterate over them in order efficiently. + unsavedFastNodeAdditions.Range(func(k, v interface{}) bool { + fastNode := v.(*fastnode.Node) + + if start != nil && bytes.Compare(fastNode.GetKey(), start) < 0 { + return true + } + + if end != nil && bytes.Compare(fastNode.GetKey(), end) >= 0 { + return true + } + + // convert key to bytes. Type conversion failure should not happen in practice + iter.unsavedFastNodesToSort = append(iter.unsavedFastNodesToSort, k.(string)) + + return true + }) + + sort.Slice(iter.unsavedFastNodesToSort, func(i, j int) bool { + if ascending { + return iter.unsavedFastNodesToSort[i] < iter.unsavedFastNodesToSort[j] + } + return iter.unsavedFastNodesToSort[i] > iter.unsavedFastNodesToSort[j] + }) + + // Move to the first element + iter.Next() + + return iter +} + +// Domain implements dbm.Iterator. +// Maps the underlying nodedb iterator domain, to the 'logical' keys involved. +func (iter *UnsavedFastIterator) Domain() ([]byte, []byte) { + return iter.start, iter.end +} + +// Valid implements dbm.Iterator. +func (iter *UnsavedFastIterator) Valid() bool { + if iter.start != nil && iter.end != nil { + if bytes.Compare(iter.end, iter.start) != 1 { + return false + } + } + + return iter.fastIterator.Valid() || iter.nextUnsavedNodeIdx < len(iter.unsavedFastNodesToSort) || (iter.nextKey != nil && iter.nextVal != nil) +} + +// Key implements dbm.Iterator +func (iter *UnsavedFastIterator) Key() []byte { + return iter.nextKey +} + +// Value implements dbm.Iterator +func (iter *UnsavedFastIterator) Value() []byte { + return iter.nextVal +} + +// Next implements dbm.Iterator +// Its effectively running the constant space overhead algorithm for streaming through sorted lists: +// the sorted lists being underlying fast nodes & unsavedFastNodeChanges +func (iter *UnsavedFastIterator) Next() { + if iter.ndb == nil { + iter.err = errFastIteratorNilNdbGiven + iter.valid = false + return + } + + diskKey := iter.fastIterator.Key() + diskKeyStr := ibytes.UnsafeBytesToStr(diskKey) + if iter.fastIterator.Valid() && iter.nextUnsavedNodeIdx < len(iter.unsavedFastNodesToSort) { + value, ok := iter.unsavedFastNodeRemovals.Load(diskKeyStr) + if ok && value != nil { + // If next fast node from disk is to be removed, skip it. + iter.fastIterator.Next() + iter.Next() + return + } + + nextUnsavedKey := iter.unsavedFastNodesToSort[iter.nextUnsavedNodeIdx] + nextUnsavedNodeVal, _ := iter.unsavedFastNodeAdditions.Load(nextUnsavedKey) + nextUnsavedNode := nextUnsavedNodeVal.(*fastnode.Node) + + var isUnsavedNext bool + if iter.ascending { + isUnsavedNext = diskKeyStr >= nextUnsavedKey + } else { + isUnsavedNext = diskKeyStr <= nextUnsavedKey + } + + if isUnsavedNext { + // Unsaved node is next + if diskKeyStr == nextUnsavedKey { + // Unsaved update prevails over saved copy so we skip the copy from disk + iter.fastIterator.Next() + } + + iter.nextKey = nextUnsavedNode.GetKey() + iter.nextVal = nextUnsavedNode.GetValue() + + iter.nextUnsavedNodeIdx++ + return + } + // Disk node is next + iter.nextKey = iter.fastIterator.Key() + iter.nextVal = iter.fastIterator.Value() + + iter.fastIterator.Next() + return + } + + // if only nodes on disk are left, we return them + if iter.fastIterator.Valid() { + value, ok := iter.unsavedFastNodeRemovals.Load(diskKeyStr) + if ok && value != nil { + // If next fast node from disk is to be removed, skip it. + iter.fastIterator.Next() + iter.Next() + return + } + + iter.nextKey = iter.fastIterator.Key() + iter.nextVal = iter.fastIterator.Value() + + iter.fastIterator.Next() + return + } + + // if only unsaved nodes are left, we can just iterate + if iter.nextUnsavedNodeIdx < len(iter.unsavedFastNodesToSort) { + nextUnsavedKey := iter.unsavedFastNodesToSort[iter.nextUnsavedNodeIdx] + nextUnsavedNodeVal, _ := iter.unsavedFastNodeAdditions.Load(nextUnsavedKey) + nextUnsavedNode := nextUnsavedNodeVal.(*fastnode.Node) + + iter.nextKey = nextUnsavedNode.GetKey() + iter.nextVal = nextUnsavedNode.GetValue() + + iter.nextUnsavedNodeIdx++ + return + } + + iter.nextKey = nil + iter.nextVal = nil +} + +// Close implements dbm.Iterator +func (iter *UnsavedFastIterator) Close() error { + iter.valid = false + return iter.fastIterator.Close() +} + +// Error implements dbm.Iterator +func (iter *UnsavedFastIterator) Error() error { + return iter.err +} diff --git a/iavl/util.go b/iavl/util.go new file mode 100644 index 000000000..3c2547512 --- /dev/null +++ b/iavl/util.go @@ -0,0 +1,63 @@ +package iavl + +import ( + "fmt" +) + +// PrintTree prints the whole tree in an indented form. +func PrintTree(tree *ImmutableTree) { + ndb, root := tree.ndb, tree.root + printNode(ndb, root, 0) //nolint:errcheck +} + +func printNode(ndb *nodeDB, node *Node, indent int) error { + indentPrefix := "" + for i := 0; i < indent; i++ { + indentPrefix += " " + } + + if node == nil { + fmt.Printf("%s\n", indentPrefix) + return nil + } + if node.rightNode != nil { + printNode(ndb, node.rightNode, indent+1) //nolint:errcheck + } else if node.rightNodeKey != nil { + rightNode, err := ndb.GetNode(node.rightNodeKey) + if err != nil { + return err + } + printNode(ndb, rightNode, indent+1) //nolint:errcheck + } + + hash := node._hash(node.nodeKey.version) + + fmt.Printf("%sh:%X\n", indentPrefix, hash) + if node.isLeaf() { + fmt.Printf("%s%X:%X (%v)\n", indentPrefix, node.key, node.value, node.subtreeHeight) + } + + if node.leftNode != nil { + err := printNode(ndb, node.leftNode, indent+1) + if err != nil { + return err + } + } else if node.leftNodeKey != nil { + leftNode, err := ndb.GetNode(node.leftNodeKey) + if err != nil { + return err + } + err = printNode(ndb, leftNode, indent+1) + if err != nil { + return err + } + } + return nil +} + +func maxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} diff --git a/iavl/version.go b/iavl/version.go new file mode 100644 index 000000000..45372de37 --- /dev/null +++ b/iavl/version.go @@ -0,0 +1,38 @@ +package iavl + +import ( + "fmt" + "runtime" +) + +// Version of iavl. Fill in fields with build flags +var ( + Version = "" + Commit = "" + Branch = "" +) + +// VersionInfo contains useful versioning information in struct +type VersionInfo struct { + IAVL string `json:"iavl"` + GitCommit string `json:"commit"` + Branch string `json:"branch"` + GoVersion string `json:"go"` +} + +func (v VersionInfo) String() string { + return fmt.Sprintf(`iavl: %s +git commit: %s +git branch: %s +%s`, v.IAVL, v.GitCommit, v.Branch, v.GoVersion) +} + +// Returns VersionInfo with global vars filled in +func GetVersionInfo() VersionInfo { + return VersionInfo{ + Version, + Commit, + Branch, + fmt.Sprintf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH), + } +} diff --git a/iavl/with_gcc_test.go b/iavl/with_gcc_test.go new file mode 100644 index 000000000..786e0a2f0 --- /dev/null +++ b/iavl/with_gcc_test.go @@ -0,0 +1,19 @@ +//go:build gcc +// +build gcc + +// This file exists because some of the DBs e.g CLevelDB +// require gcc as the compiler before they can ran otherwise +// we'll encounter crashes such as in https://github.com/tendermint/merkleeyes/issues/39 + +package iavl + +import ( + "testing" + + db "github.com/cosmos/cosmos-db" +) + +func BenchmarkImmutableAvlTreeCLevelDB(b *testing.B) { + db := db.NewDB("test", db.CLevelDBBackendStr, "./") + benchmarkImmutableAvlTreeWithDB(b, db) +}