├── .github ├── demo.gif ├── dependabot.yaml ├── golangci.yaml ├── goreleaser.yaml └── workflows │ ├── dagger.yaml │ ├── lint.yaml │ ├── main.yaml │ ├── release-binary.yml │ └── test.yaml ├── .gitignore ├── Dockerfile.release ├── LICENSE ├── Makefile ├── README.md ├── ci └── dagger │ ├── dagger.json │ └── run-supernova │ ├── .gitattributes │ ├── .gitignore │ ├── go.mod │ ├── go.sum │ └── main.go ├── cmd └── root.go ├── go.mod ├── go.sum ├── internal ├── batcher │ ├── batcher.go │ ├── batcher_test.go │ ├── mock_test.go │ └── types.go ├── client │ ├── batch.go │ └── client.go ├── collector │ ├── collector.go │ ├── collector_test.go │ ├── mock_test.go │ └── types.go ├── common │ ├── common.go │ └── types.go ├── config.go ├── distributor │ ├── distributor.go │ ├── distributor_test.go │ └── mock_test.go ├── output.go ├── pipeline.go ├── runtime │ ├── helper.go │ ├── helper_test.go │ ├── package_deployment.go │ ├── realm_call.go │ ├── realm_deployment.go │ ├── runtime.go │ ├── runtime_test.go │ ├── source.go │ ├── type.go │ └── type_test.go ├── signer │ └── signer.go └── testing │ └── testing.go └── tools ├── go.mod ├── go.sum └── tools.go /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnolang/supernova/9d440729feb7f7fa306c11b46a12b038f10efd7b/.github/demo.gif -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | labels: 10 | - "github_actions" 11 | 12 | # Maintain dependencies for top level Go modules 13 | - package-ecosystem: gomod 14 | directory: / 15 | target-branch: "main" 16 | schedule: 17 | interval: weekly 18 | labels: 19 | - "dependencies" 20 | groups: 21 | golang-x: 22 | patterns: 23 | - "golang.org/x/*" 24 | everything-else: 25 | patterns: 26 | - "*" 27 | open-pull-requests-limit: 10 28 | pull-request-branch-name: 29 | separator: "-" 30 | reviewers: 31 | - "zivkovicmilos" 32 | -------------------------------------------------------------------------------- /.github/golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 8 3 | timeout: 10m 4 | issue-exit-code: 1 5 | tests: true 6 | modules-download-mode: readonly 7 | allow-parallel-runners: false 8 | go: "" 9 | 10 | output: 11 | path-prefix: "" 12 | sort-results: true 13 | 14 | issues: 15 | max-issues-per-linter: 0 16 | max-same-issues: 0 17 | new: false 18 | fix: false 19 | exclude-rules: 20 | - path: (.+)_test.go 21 | linters: 22 | - nilnil 23 | - gosec 24 | exclude-dirs-use-default: true 25 | uniq-by-line: false 26 | 27 | linters: 28 | fast: false 29 | disable-all: true 30 | enable: 31 | - asasalint # Check for pass []any as any in variadic func(...any) 32 | - asciicheck # Detects funky ASCII characters 33 | - bidichk # Checks for dangerous unicode character sequences 34 | - durationcheck # Check for two durations multiplied together 35 | - errcheck # Forces to not skip error check 36 | - copyloopvar # Checks for pointers to enclosing loop variables 37 | - gocritic # Bundles different linting checks 38 | - godot # Checks for periods at the end of comments 39 | - gomoddirectives # Allow or ban replace directives in go.mod 40 | - gosimple # Code simplification 41 | - govet # Official Go tool 42 | - ineffassign # Detects when assignments to existing variables are not used 43 | - nakedret # Finds naked/bare returns and requires change them 44 | - nilerr # Requires explicit returns 45 | - nilnil # Requires explicit returns 46 | - promlinter # Lints Prometheus metrics names 47 | - reassign # Checks that package variables are not reassigned 48 | - revive # Drop-in replacement for golint 49 | - tenv # Detects using os.Setenv instead of t.Setenv 50 | - testableexamples # Checks if examples are testable (have expected output) 51 | - unparam # Finds unused params 52 | - usestdlibvars # Detects the possibility to use variables/constants from stdlib 53 | - wastedassign # Finds wasted assignment statements 54 | - loggercheck # Checks the odd number of key and value pairs for common logger libraries 55 | - nestif # Finds deeply nested if statements 56 | - nonamedreturns # Reports all named returns 57 | - decorder # Check declaration order of types, consts, vars and funcs 58 | - gocheckcompilerdirectives # Checks that compiler directive comments (//go:) are valid 59 | - gochecknoinits # Checks for init methods 60 | - whitespace # Tool for detection of leading and trailing whitespace 61 | - wsl # Forces you to use empty lines 62 | - unconvert # Unnecessary type conversions 63 | - tparallel # Detects inappropriate usage of t.Parallel() method in your Go test codes 64 | - thelper # Detects golang test helpers without t.Helper() call and checks the consistency of test helpers 65 | - stylecheck # Stylecheck is a replacement for golint 66 | - prealloc # Finds slice declarations that could potentially be pre-allocated 67 | - predeclared # Finds code that shadows one of Go's predeclared identifiers 68 | - nolintlint # Ill-formed or insufficient nolint directives 69 | - nlreturn # Checks for a new line before return and branch statements to increase code clarity 70 | - misspell # Misspelled English words in comments 71 | - makezero # Finds slice declarations with non-zero initial length 72 | - lll # Long lines 73 | - importas # Enforces consistent import aliases 74 | - gosec # Security problems 75 | - gofmt # Whether the code was gofmt-ed 76 | - gofumpt # Stricter gofmt 77 | - goimports # Unused imports 78 | - goconst # Repeated strings that could be replaced by a constant 79 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) 80 | - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 81 | - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13 82 | - unused # Checks Go code for unused constants, variables, functions and types 83 | 84 | linters-settings: 85 | gocritic: 86 | enabled-tags: 87 | - diagnostic 88 | - experimental 89 | - opinionated 90 | - performance 91 | - style 92 | disabled-checks: 93 | - hugeParam 94 | - rangeExprCopy 95 | - rangeValCopy 96 | - importShadow 97 | - unnamedResult 98 | errcheck: 99 | check-type-assertions: false 100 | check-blank: true 101 | exclude-functions: 102 | - io/ioutil.ReadFile 103 | - io.Copy(*bytes.Buffer) 104 | - io.Copy(os.Stdout) 105 | nakedret: 106 | max-func-lines: 1 107 | govet: 108 | enable-all: true 109 | gofmt: 110 | simplify: true 111 | goconst: 112 | min-len: 3 113 | min-occurrences: 3 114 | godot: 115 | scope: all 116 | period: false 117 | gosec: 118 | excludes: 119 | - G115 # We tolerate casting shenanigans -------------------------------------------------------------------------------- /.github/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | project_name: supernova 3 | version: 2 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | 9 | builds: 10 | - main: ./cmd 11 | binary: supernova 12 | env: 13 | - CGO_ENABLED=0 14 | goos: 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - arm64 20 | 21 | gomod: 22 | proxy: true 23 | 24 | archives: 25 | # https://goreleaser.com/customization/archive/ 26 | - files: 27 | # Standard Release Files 28 | - LICENSE 29 | - README.md 30 | 31 | signs: 32 | - cmd: cosign 33 | env: 34 | - COSIGN_EXPERIMENTAL=1 35 | certificate: '${artifact}.pem' 36 | args: 37 | - sign-blob 38 | - '--output-certificate=${certificate}' 39 | - '--output-signature=${signature}' 40 | - '${artifact}' 41 | - "--yes" # needed on cosign 2.0.0+ 42 | artifacts: checksum 43 | output: true 44 | 45 | dockers: 46 | # https://goreleaser.com/customization/docker/ 47 | - use: buildx 48 | dockerfile: Dockerfile.release 49 | goos: linux 50 | goarch: amd64 51 | image_templates: 52 | - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64" 53 | - "ghcr.io/gnolang/{{ .ProjectName }}:latest-amd64" 54 | build_flag_templates: 55 | - "--platform=linux/amd64" 56 | - "--label=org.opencontainers.image.created={{.Date}}" 57 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 58 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 59 | - "--label=org.opencontainers.image.version={{.Version}}" 60 | - use: buildx 61 | dockerfile: Dockerfile.release 62 | goos: linux 63 | goarch: arm64 64 | image_templates: 65 | - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8" 66 | - "ghcr.io/gnolang/{{ .ProjectName }}:latest-arm64v8" 67 | build_flag_templates: 68 | - "--platform=linux/arm64/v8" 69 | - "--label=org.opencontainers.image.created={{.Date}}" 70 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 71 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 72 | - "--label=org.opencontainers.image.version={{.Version}}" 73 | 74 | docker_manifests: 75 | # https://goreleaser.com/customization/docker_manifest/ 76 | - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }} 77 | image_templates: 78 | - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 79 | - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 80 | - name_template: ghcr.io/gnolang/{{ .ProjectName }}:latest 81 | image_templates: 82 | - ghcr.io/gnolang/{{ .ProjectName }}:latest-amd64 83 | - ghcr.io/gnolang/{{ .ProjectName }}:latest-arm64v8 84 | 85 | docker_signs: 86 | - cmd: cosign 87 | env: 88 | - COSIGN_EXPERIMENTAL=1 89 | artifacts: images 90 | output: true 91 | args: 92 | - 'sign' 93 | - '${artifact}' 94 | - "--yes" # needed on cosign 2.0.0+ 95 | 96 | checksum: 97 | name_template: 'checksums.txt' 98 | 99 | changelog: 100 | sort: asc 101 | 102 | source: 103 | enabled: true 104 | 105 | sboms: 106 | - artifacts: archive 107 | - id: source # Two different sbom configurations need two different IDs 108 | artifacts: source 109 | 110 | release: 111 | draft: true 112 | replace_existing_draft: true 113 | prerelease: auto 114 | footer: | 115 | ### Container Images 116 | 117 | https://ghcr.io/gnolang/{{ .ProjectName }}:{{ .Tag }} 118 | 119 | For example: 120 | ``` 121 | docker pull ghcr.io/gnolang/{{ .ProjectName }}:{{ .Tag }} 122 | ``` 123 | -------------------------------------------------------------------------------- /.github/workflows/dagger.yaml: -------------------------------------------------------------------------------- 1 | name: Dagger Supernova 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 1 * * *' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | ref: main 15 | 16 | supernova: 17 | name: supernova-ci 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Supernova Run 23 | uses: dagger/dagger-for-github@v7 24 | env: 25 | RPC_URL: https://rpc.gno.land 26 | CHAIN_ID: portal-loop 27 | with: 28 | version: "v0.15.2" 29 | verb: call 30 | module: ci/dagger 31 | args: run-stress-test --src-dir . --chain-id ${CHAIN_ID} --rpc-endpoint ${RPC_URL} --sub-accounts 2 --transactions 10 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Go Code 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: 1.23 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Lint 20 | uses: golangci/golangci-lint-action@v7 21 | with: 22 | version: v1.63 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Main Branch - Build & Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths: 9 | - '**/*.go' 10 | - 'go.mod' 11 | - 'go.sum' 12 | 13 | jobs: 14 | lint: 15 | name: Go Linter 16 | uses: ./.github/workflows/lint.yaml 17 | 18 | test: 19 | name: Go Test 20 | uses: ./.github/workflows/test.yaml 21 | -------------------------------------------------------------------------------- /.github/workflows/release-binary.yml: -------------------------------------------------------------------------------- 1 | name: Release Binaries 2 | 3 | permissions: 4 | contents: write # needed to write releases 5 | id-token: write # needed for keyless signing 6 | packages: write # needed for ghcr access 7 | 8 | on: 9 | push: 10 | tags: 11 | - "v*" 12 | 13 | jobs: 14 | binary: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.23 21 | 22 | - uses: sigstore/cosign-installer@v3.8.1 23 | - uses: anchore/sbom-action/download-syft@v0.18.0 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.repository_owner }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Set up QEMU 37 | uses: docker/setup-qemu-action@v3 38 | 39 | - name: Run GoReleaser 40 | uses: goreleaser/goreleaser-action@v6 41 | with: 42 | version: latest 43 | args: release --clean --config ./.github/goreleaser.yaml 44 | env: 45 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Go Tests 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: 1.23 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Go test 20 | run: go test -shuffle=on -coverprofile coverage.out -timeout 5m ./... 21 | 22 | test-with-race: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Install Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: 1.23 29 | 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Go race test 34 | run: go test -race -shuffle=on -timeout 5m ./... 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS Leftovers 2 | .DS_Store 3 | 4 | # Editor Leftovers 5 | .vscode 6 | .idea 7 | 8 | # Build Leftovers 9 | build/* 10 | 11 | # Results 12 | *.json 13 | !ci/**/*.json 14 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | # NOTE: using `scratch` as BASE image would lack of CA certicates 2 | FROM golang:1.23-alpine 3 | COPY supernova /usr/bin/supernova 4 | ENTRYPOINT [ "/usr/bin/supernova" ] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | golangci_lint := go run -modfile=./tools/go.mod github.com/golangci/golangci-lint/cmd/golangci-lint 2 | 3 | all: build 4 | 5 | .PHONY: build 6 | build: 7 | @echo "Building supernova binary" 8 | go build -o build/supernova ./cmd 9 | 10 | test: 11 | go test -v ./... 12 | 13 | .PHONY: lint 14 | lint: 15 | $(golangci_lint) run --config .github/golangci.yaml 16 | 17 | .PHONY: gofumpt 18 | gofumpt: 19 | go install mvdan.cc/gofumpt@latest 20 | gofumpt -l -w . 21 | 22 | .PHONY: fixalign 23 | fixalign: 24 | go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest 25 | fieldalignment -fix $(filter-out $@,$(MAKECMDGOALS)) # the full package name (not path!) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | `supernova` is a command-line interface (CLI) tool for stress-testing Gno Tendermint 2 networks. It is used to monitor 4 | and report on node performance by executing transactions and measuring response-time. 5 | 6 | ## Key Features 7 | 8 | - 🚀 Batch transactions to make stress testing easier to orchestrate 9 | - 🛠 Multiple stress testing modes: REALM_DEPLOYMENT, PACKAGE_DEPLOYMENT, and REALM_CALL 10 | - 💰 Distributed transaction stress testing through subaccounts 11 | - 💸 Automatic subaccount fund top-up 12 | - 📊 Detailed statistics calculation 13 | - 📈 Output cycle run results to a file 14 | 15 | ## Results 16 | 17 | To view the results of the stress tests, visit the [benchmarks reports for supernova](https://github.com/gnolang/benchmarks/tree/main/reports/supernova). 18 | 19 | ## Usage Example 20 | 21 | To run a stress test with `supernova`, you will need to have `go 1.19` or greater. 22 | 23 | 1. Build out the binary 24 | 25 | To build out the binary, run the following command: 26 | 27 | ```bash 28 | make build 29 | ``` 30 | 31 | 2. Run the stress test by specifying options 32 | 33 | ```bash 34 | ./build/supernova -sub-accounts 5 -transactions 100 -url http://localhost:26657 -mnemonic "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" -output result.json 35 | ``` 36 | 37 | This will run a stress test against a Gno TM2 node running at `http://localhost:26657`. The test will use `5` 38 | sub-accounts, and send out `100` transactions. The sub-accounts are derived from the specified mnemonic. Finally, 39 | results are saved 40 | to a file `result.json`. 41 | 42 | For any stress test run, there need to be funds on a specific address. 43 | The address that is in charge of funds distribution to subaccounts is the **first address** with index 0 in the 44 | specified mnemonic. Make sure this address has an appropriate amount of funds before running the stress test. 45 | 46 | ![Banner](.github/demo.gif) 47 | 48 | `supernova` supports the following options: 49 | 50 | ```bash 51 | USAGE 52 | [flags] [...] 53 | 54 | Starts the stress testing suite against a Gno TM2 cluster 55 | 56 | FLAGS 57 | -batch 100 the batch size of JSON-RPC transactions 58 | -chain-id dev the chain ID of the Gno blockchain 59 | -mnemonic string the mnemonic used to generate sub-accounts 60 | -mode REALM_DEPLOYMENT the mode for the stress test. Possible modes: [REALM_DEPLOYMENT, PACKAGE_DEPLOYMENT, REALM_CALL] 61 | -output string the output path for the results JSON 62 | -sub-accounts 10 the number of sub-accounts that will send out transactions 63 | -transactions 100 the total number of transactions to be emitted 64 | -url string the JSON-RPC URL of the cluster 65 | ``` 66 | 67 | ## Modes 68 | 69 | ### REALM_DEPLOYMENT 70 | 71 | The `REALM_DEPLOYMENT` mode is pretty straightforward - it is a simple `Realm` deployment mode from accounts. 72 | This mode sends out transactions that are deploy transactions for a realm holding state. 73 | 74 | ### PACKAGE_DEPLOYMENT 75 | 76 | The `PACKAGE_DEPLOYMENT` is similar to `REALM_DEPLOYMENT`. This mode also sends out transactions, but these transactions 77 | deploy a package. 78 | 79 | ### REALM_CALL 80 | 81 | The `REALM_CALL` mode deploys a `Realm` to the Gno blockchain network being tested before starting the cycle run. 82 | When the cycle run begins, the transactions that are sent out are method calls. 83 | -------------------------------------------------------------------------------- /ci/dagger/dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supernova", 3 | "engineVersion": "v0.15.2", 4 | "sdk": "go", 5 | "source": "run-supernova" 6 | } 7 | -------------------------------------------------------------------------------- /ci/dagger/run-supernova/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /internal/dagger/** linguist-generated 3 | /internal/querybuilder/** linguist-generated 4 | /internal/telemetry/** linguist-generated 5 | -------------------------------------------------------------------------------- /ci/dagger/run-supernova/.gitignore: -------------------------------------------------------------------------------- 1 | /dagger.gen.go 2 | /internal/dagger 3 | /internal/querybuilder 4 | /internal/telemetry 5 | -------------------------------------------------------------------------------- /ci/dagger/run-supernova/go.mod: -------------------------------------------------------------------------------- 1 | module dagger/dagger 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.57 7 | github.com/Khan/genqlient v0.7.0 8 | github.com/vektah/gqlparser/v2 v2.5.20 9 | go.opentelemetry.io/otel v1.27.0 10 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 11 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 12 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 13 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 14 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 15 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 16 | go.opentelemetry.io/otel/log v0.3.0 17 | go.opentelemetry.io/otel/metric v1.27.0 18 | go.opentelemetry.io/otel/sdk v1.27.0 19 | go.opentelemetry.io/otel/sdk/log v0.3.0 20 | go.opentelemetry.io/otel/sdk/metric v1.27.0 21 | go.opentelemetry.io/otel/trace v1.27.0 22 | go.opentelemetry.io/proto/otlp v1.3.1 23 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa 24 | golang.org/x/sync v0.10.0 25 | google.golang.org/grpc v1.68.0 26 | ) 27 | 28 | require ( 29 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 30 | github.com/go-logr/logr v1.4.2 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/google/uuid v1.6.0 // indirect 33 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect 34 | github.com/sosodev/duration v1.3.1 // indirect 35 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect 36 | golang.org/x/net v0.33.0 // indirect 37 | golang.org/x/sys v0.28.0 // indirect 38 | golang.org/x/text v0.21.0 // indirect 39 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect 40 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 41 | google.golang.org/protobuf v1.35.2 // indirect 42 | ) 43 | 44 | replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 45 | 46 | replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 47 | 48 | replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.3.0 49 | 50 | replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.3.0 51 | -------------------------------------------------------------------------------- /ci/dagger/run-supernova/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.17.57 h1:Ak4p60BRq6QibxY0lEc0JnQhDurfhxA67sp02lMjmPc= 2 | github.com/99designs/gqlgen v0.17.57/go.mod h1:Jx61hzOSTcR4VJy/HFIgXiQ5rJ0Ypw8DxWLjbYDAUw0= 3 | github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= 4 | github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= 5 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 7 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 8 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 12 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 13 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 15 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 16 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 17 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 21 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= 23 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 27 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 28 | github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= 29 | github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= 30 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 31 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 32 | github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo= 33 | github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= 34 | go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= 35 | go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= 36 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= 37 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= 38 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 h1:ccBrA8nCY5mM0y5uO7FT0ze4S0TuFcWdDB2FxGMTjkI= 39 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0/go.mod h1:/9pb6634zi2Lk8LYg9Q0X8Ar6jka4dkFOylBLbVQPCE= 40 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= 41 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= 42 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= 43 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= 44 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= 45 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= 46 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= 47 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= 48 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= 49 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= 50 | go.opentelemetry.io/otel/log v0.3.0 h1:kJRFkpUFYtny37NQzL386WbznUByZx186DpEMKhEGZs= 51 | go.opentelemetry.io/otel/log v0.3.0/go.mod h1:ziCwqZr9soYDwGNbIL+6kAvQC+ANvjgG367HVcyR/ys= 52 | go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= 53 | go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= 54 | go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= 55 | go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= 56 | go.opentelemetry.io/otel/sdk/log v0.3.0 h1:GEjJ8iftz2l+XO1GF2856r7yYVh74URiF9JMcAacr5U= 57 | go.opentelemetry.io/otel/sdk/log v0.3.0/go.mod h1:BwCxtmux6ACLuys1wlbc0+vGBd+xytjmjajwqqIul2g= 58 | go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= 59 | go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= 60 | go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= 61 | go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= 62 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 63 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 64 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 65 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 66 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 67 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 68 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 69 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 70 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 71 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 72 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 73 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 74 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 75 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 76 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= 77 | google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= 78 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 79 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 80 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= 81 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= 82 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 83 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | -------------------------------------------------------------------------------- /ci/dagger/run-supernova/main.go: -------------------------------------------------------------------------------- 1 | // A generated module for Dagger functions 2 | // 3 | // This module has been generated via dagger init and serves as a reference to 4 | // basic module structure as you get started with Dagger. 5 | // 6 | // Two functions have been pre-created. You can modify, delete, or add to them, 7 | // as needed. They demonstrate usage of arguments and return types using simple 8 | // echo and grep commands. The functions can be called from the dagger CLI or 9 | // from one of the SDKs. 10 | // 11 | // The first line in this comment block is a short description line and the 12 | // rest is a long description with more detail on the module's purpose or usage, 13 | // if appropriate. All modules should have a short description. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "dagger/dagger/internal/dagger" 20 | "fmt" 21 | ) 22 | 23 | type RunSupernova struct{} 24 | 25 | const ( 26 | DEFAULT_CHAINID = "dev" 27 | DEFAULT_SUBACCOUNTS = 1 28 | DEFAULT_TRANSACTIONS = 10 29 | MNEMONIC = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" 30 | ) 31 | 32 | type Supernova struct{} 33 | 34 | // Builds Supernova image from code passed into a *dagger.Directory item 35 | func (s *Supernova) BuildImage(directory *dagger.Directory) *dagger.Container { 36 | baseBuilder := dag.Container(). 37 | From("golang:1.23-alpine"). 38 | WithDirectory("/src", directory). 39 | WithWorkdir("/src"). 40 | WithEnvVariable("CGO_ENABLED", "0"). 41 | WithExec([]string{"go", "build", "-o", "supernova", "./cmd"}) 42 | 43 | return dag.Container(). 44 | From("busybox"). 45 | WithFile("/bin/supernova", baseBuilder.File("/src/supernova")). 46 | WithFile("/etc/ssl/certs/", baseBuilder.File("/etc/ssl/certs/ca-certificates.crt")). 47 | WithEntrypoint([]string{"/bin/supernova"}) 48 | } 49 | 50 | // Build image from code or use latest prebuild Docker image 51 | func (s *Supernova) buildOrPull(srcDir *dagger.Directory) *dagger.Container { 52 | if srcDir == nil { 53 | return dag.Container(). 54 | From("ghcr.io/gnolang/supernova:latest") 55 | } 56 | return s.BuildImage(srcDir) 57 | } 58 | 59 | // Runs a simple Supernova task generating transactions 60 | func (s *Supernova) RunStressTest( 61 | ctx context.Context, 62 | rpcEndpoint string, 63 | // +optional 64 | chainId string, 65 | // +optional 66 | subAccounts int, 67 | // +optional 68 | transactions int, 69 | // + optional 70 | srcDir *dagger.Directory, 71 | ) (int, error) { 72 | 73 | if chainId == "" { 74 | chainId = DEFAULT_CHAINID 75 | } 76 | if subAccounts == 0 { 77 | subAccounts = DEFAULT_SUBACCOUNTS 78 | } 79 | if transactions == 0 { 80 | transactions = DEFAULT_TRANSACTIONS 81 | } 82 | 83 | return s.buildOrPull(srcDir). 84 | WithExec([]string{ 85 | "-sub-accounts", fmt.Sprintf("%d", subAccounts), 86 | "-transactions", fmt.Sprintf("%d", transactions), 87 | "-chain-id", chainId, 88 | "-url", rpcEndpoint, 89 | "-mnemonic", MNEMONIC}, 90 | dagger.ContainerWithExecOpts{ 91 | UseEntrypoint: true, 92 | }). 93 | ExitCode(ctx) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/gnolang/supernova/internal" 10 | "github.com/gnolang/supernova/internal/runtime" 11 | "github.com/peterbourgon/ff/v3/ffcli" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | cfg = &internal.Config{} 17 | fs = flag.NewFlagSet("pipeline", flag.ExitOnError) 18 | ) 19 | 20 | // Register the flags 21 | registerFlags(fs, cfg) 22 | 23 | cmd := &ffcli.Command{ 24 | ShortUsage: "[flags] [...]", 25 | LongHelp: "Starts the stress testing suite against a Gno TM2 cluster", 26 | FlagSet: fs, 27 | Exec: func(_ context.Context, _ []string) error { 28 | return execMain(cfg) 29 | }, 30 | } 31 | 32 | if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { 33 | _, _ = fmt.Fprintf(os.Stderr, "%+v", err) 34 | 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | // registerFlags registers the main configuration flags 40 | func registerFlags(fs *flag.FlagSet, c *internal.Config) { 41 | fs.StringVar( 42 | &c.URL, 43 | "url", 44 | "", 45 | "the JSON-RPC URL of the cluster", 46 | ) 47 | 48 | fs.StringVar( 49 | &c.ChainID, 50 | "chain-id", 51 | "dev", 52 | "the chain ID of the Gno blockchain", 53 | ) 54 | 55 | fs.StringVar( 56 | &c.Mnemonic, 57 | "mnemonic", 58 | "", 59 | "the mnemonic used to generate sub-accounts", 60 | ) 61 | 62 | fs.StringVar( 63 | &c.Mode, 64 | "mode", 65 | runtime.RealmDeployment.String(), 66 | fmt.Sprintf( 67 | "the mode for the stress test. Possible modes: [%s, %s, %s]", 68 | runtime.RealmDeployment.String(), runtime.PackageDeployment.String(), runtime.RealmCall.String(), 69 | ), 70 | ) 71 | 72 | fs.StringVar( 73 | &c.Output, 74 | "output", 75 | "", 76 | "the output path for the results JSON", 77 | ) 78 | 79 | fs.Uint64Var( 80 | &c.SubAccounts, 81 | "sub-accounts", 82 | 10, 83 | "the number of sub-accounts that will send out transactions", 84 | ) 85 | 86 | fs.Uint64Var( 87 | &c.Transactions, 88 | "transactions", 89 | 100, 90 | "the total number of transactions to be emitted", 91 | ) 92 | 93 | fs.Uint64Var( 94 | &c.BatchSize, 95 | "batch", 96 | 100, 97 | "the batch size of JSON-RPC transactions", 98 | ) 99 | } 100 | 101 | // execMain starts the stress test workflow (runs the pipeline) 102 | func execMain(cfg *internal.Config) error { 103 | // Validate the configuration 104 | if err := cfg.Validate(); err != nil { 105 | return fmt.Errorf("invalid configuration, %w", err) 106 | } 107 | 108 | // Create and run the pipeline 109 | pipeline, err := internal.NewPipeline(cfg) 110 | if err != nil { 111 | return fmt.Errorf("unable to create pipeline, %w", err) 112 | } 113 | 114 | return pipeline.Execute() 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gnolang/supernova 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gnolang/gno v0.0.0-20250129165357-b392287f0d2c 7 | github.com/peterbourgon/ff/v3 v3.4.0 8 | github.com/schollz/progressbar/v3 v3.18.0 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | dario.cat/mergo v1.0.1 // indirect 14 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect 15 | github.com/btcsuite/btcd/btcutil v1.1.6 // indirect 16 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 17 | github.com/cockroachdb/apd/v3 v3.2.1 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 20 | github.com/fsnotify/fsnotify v1.5.4 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | github.com/golang/snappy v0.0.4 // indirect 24 | github.com/google/uuid v1.6.0 // indirect 25 | github.com/gorilla/websocket v1.5.3 // indirect 26 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect 27 | github.com/lib/pq v1.10.9 // indirect 28 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 29 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 30 | github.com/onsi/gomega v1.31.1 // indirect 31 | github.com/pelletier/go-toml v1.9.5 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/rivo/uniseg v0.4.7 // indirect 34 | github.com/rs/cors v1.11.1 // indirect 35 | github.com/rs/xid v1.6.0 // indirect 36 | github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect 37 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 38 | go.etcd.io/bbolt v1.3.11 // indirect 39 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 40 | go.opentelemetry.io/otel v1.34.0 // indirect 41 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect 42 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 // indirect 43 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 44 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 45 | go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect 46 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 47 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 48 | go.uber.org/multierr v1.11.0 // indirect 49 | golang.org/x/crypto v0.32.0 // indirect 50 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect 51 | golang.org/x/mod v0.22.0 // indirect 52 | golang.org/x/net v0.34.0 // indirect 53 | golang.org/x/sync v0.10.0 // indirect 54 | golang.org/x/sys v0.29.0 // indirect 55 | golang.org/x/term v0.28.0 // indirect 56 | golang.org/x/text v0.21.0 // indirect 57 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect 58 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 59 | google.golang.org/grpc v1.69.4 // indirect 60 | google.golang.org/protobuf v1.36.3 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 4 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 5 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 6 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= 7 | github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= 8 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= 9 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 10 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 11 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= 12 | github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 13 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 14 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 15 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= 16 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= 17 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= 18 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 19 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 20 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= 21 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 22 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 23 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 24 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 25 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 26 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 27 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 28 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 29 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 30 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 31 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 32 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 33 | github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= 34 | github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= 35 | github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= 36 | github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= 37 | github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= 38 | github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= 39 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 44 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 45 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 46 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 47 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 48 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 49 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 50 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 51 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 52 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 53 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 54 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 55 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 56 | github.com/gnolang/gno v0.0.0-20250129165357-b392287f0d2c h1:9XalLZCqb6vfNUXojzVj4WXgpQEAONsyeoAH+DTbT1E= 57 | github.com/gnolang/gno v0.0.0-20250129165357-b392287f0d2c/go.mod h1:9UN8hxUrbTQzq5oqXdx39seAtRBhUyONGw0voV4lMBs= 58 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 59 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 60 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 61 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 62 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 63 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 65 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 66 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 67 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 68 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 69 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 70 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 71 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 72 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 73 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 74 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 75 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 76 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 77 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 78 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 79 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 80 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 82 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 83 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 84 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 85 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 86 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= 87 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= 88 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 89 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 90 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 91 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 92 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 93 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 94 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 95 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 96 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 97 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 98 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 99 | github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 100 | github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 101 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 102 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 103 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 104 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 105 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 106 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 107 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 108 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 109 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 110 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 111 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 112 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 113 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 114 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 115 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 116 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 117 | github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= 118 | github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= 119 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 120 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 121 | github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= 122 | github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 123 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 124 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 125 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 126 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 128 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 129 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 130 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 131 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 132 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 133 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 134 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 135 | github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= 136 | github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= 137 | github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= 138 | github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= 139 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 140 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 141 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 142 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 144 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 145 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 146 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 147 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 148 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 149 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 150 | github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= 151 | github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= 152 | github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= 153 | github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= 154 | go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= 155 | go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= 156 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 157 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 158 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 159 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 160 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= 161 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= 162 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 h1:opwv08VbCZ8iecIWs+McMdHRcAXzjAeda3uG2kI/hcA= 163 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0/go.mod h1:oOP3ABpW7vFHulLpE8aYtNBodrHhMTrvfxUXGvqm7Ac= 164 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 165 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 166 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 167 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 168 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 169 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 170 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 171 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 172 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 173 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 174 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 175 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 176 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 177 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 178 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 179 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 180 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 181 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= 182 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 183 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 184 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 185 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 187 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 188 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 189 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 190 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 191 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 192 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 194 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 195 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 196 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 205 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 206 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 207 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 208 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 209 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 210 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 211 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 212 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 213 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 214 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 215 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 216 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 217 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= 218 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= 219 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 220 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 221 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 222 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 223 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 224 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 225 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 226 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 227 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 228 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 229 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 230 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 231 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 232 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 233 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 234 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 235 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 236 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 237 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 238 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 239 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 240 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 241 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 242 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 243 | -------------------------------------------------------------------------------- /internal/batcher/batcher.go: -------------------------------------------------------------------------------- 1 | package batcher 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/gnolang/gno/tm2/pkg/amino" 9 | core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 10 | "github.com/gnolang/gno/tm2/pkg/std" 11 | "github.com/gnolang/supernova/internal/common" 12 | "github.com/schollz/progressbar/v3" 13 | ) 14 | 15 | // Batcher batches signed transactions 16 | // to the Gno Tendermint node 17 | type Batcher struct { 18 | cli Client 19 | } 20 | 21 | // NewBatcher creates a new Batcher instance 22 | func NewBatcher(cli Client) *Batcher { 23 | return &Batcher{ 24 | cli: cli, 25 | } 26 | } 27 | 28 | // BatchTransactions batches provided transactions using the 29 | // specified batch size 30 | func (b *Batcher) BatchTransactions(txs []*std.Tx, batchSize int) (*TxBatchResult, error) { 31 | fmt.Printf("\n📦 Batching Transactions 📦\n\n") 32 | 33 | // Note the current latest block 34 | latest, err := b.cli.GetLatestBlockHeight() 35 | if err != nil { 36 | return nil, fmt.Errorf("unable to fetch latest block %w", err) 37 | } 38 | 39 | fmt.Printf("Latest block number: %d\n", latest) 40 | 41 | // Marshal the transactions 42 | fmt.Printf("\nPreparing transactions...\n") 43 | 44 | preparedTxs, err := prepareTransactions(txs) 45 | if err != nil { 46 | return nil, fmt.Errorf("unable to batch transactions, %w", err) 47 | } 48 | 49 | // Generate the batches 50 | readyBatches, err := b.generateBatches(preparedTxs, batchSize) 51 | if err != nil { 52 | return nil, fmt.Errorf("unable to generate batches, %w", err) 53 | } 54 | 55 | // Execute the batch requests. 56 | // Batch requests need to be sent out sequentially 57 | // to preserve account sequence order 58 | batchResults, err := sendBatches(readyBatches) 59 | if err != nil { 60 | return nil, fmt.Errorf("unable to send batches, %w", err) 61 | } 62 | 63 | // Parse the results 64 | txHashes, err := parseBatchResults(batchResults, len(txs)) 65 | if err != nil { 66 | return nil, fmt.Errorf("unable to parse batch results, %w", err) 67 | } 68 | 69 | fmt.Printf("✅ Successfully sent %d txs in %d batches\n", len(txs), len(readyBatches)) 70 | 71 | return &TxBatchResult{ 72 | TxHashes: txHashes, 73 | StartBlock: latest, 74 | }, nil 75 | } 76 | 77 | // prepareTransactions marshals the transactions into amino binary 78 | func prepareTransactions(txs []*std.Tx) ([][]byte, error) { 79 | marshalledTxs := make([][]byte, len(txs)) 80 | bar := progressbar.Default(int64(len(txs)), "txs prepared") 81 | 82 | for index, tx := range txs { 83 | txBin, err := amino.Marshal(tx) 84 | if err != nil { 85 | return nil, fmt.Errorf("unable to marshal tx, %w", err) 86 | } 87 | 88 | marshalledTxs[index] = txBin 89 | 90 | _ = bar.Add(1) //nolint:errcheck // No need to check 91 | } 92 | 93 | return marshalledTxs, nil 94 | } 95 | 96 | // generateBatches generates batches of transactions 97 | func (b *Batcher) generateBatches(txs [][]byte, batchSize int) ([]common.Batch, error) { 98 | var ( 99 | batches = generateBatches(txs, batchSize) 100 | numBatches = len(batches) 101 | readyBatches = make([]common.Batch, numBatches) 102 | ) 103 | 104 | fmt.Printf("\nGenerating batches...\n") 105 | 106 | bar := progressbar.Default(int64(numBatches), "batches generated") 107 | 108 | for index, batch := range batches { 109 | cliBatch := b.cli.CreateBatch() 110 | 111 | for _, tx := range batch { 112 | // Append the transaction 113 | if err := cliBatch.AddTxBroadcast(tx); err != nil { 114 | return nil, fmt.Errorf("unable to prepare transaction, %w", err) 115 | } 116 | } 117 | 118 | readyBatches[index] = cliBatch 119 | 120 | _ = bar.Add(1) //nolint:errcheck // No need to check 121 | } 122 | 123 | return readyBatches, nil 124 | } 125 | 126 | // sendBatches sends the prepared batch requests 127 | func sendBatches(readyBatches []common.Batch) ([][]any, error) { 128 | var ( 129 | numBatches = len(readyBatches) 130 | batchResults = make([][]any, numBatches) 131 | ) 132 | 133 | fmt.Printf("\nSending batches...\n") 134 | 135 | bar := progressbar.Default(int64(numBatches), "batches sent") 136 | 137 | for index, readyBatch := range readyBatches { 138 | batchResult, err := readyBatch.Execute() 139 | if err != nil { 140 | return nil, fmt.Errorf("unable to batch request, %w", err) 141 | } 142 | 143 | batchResults[index] = batchResult 144 | 145 | _ = bar.Add(1) //nolint:errcheck // No need to check 146 | } 147 | 148 | fmt.Printf("✅ Successfully sent %d batches\n", numBatches) 149 | 150 | return batchResults, nil 151 | } 152 | 153 | // parseBatchResults extracts transaction hashes 154 | // from batch results 155 | func parseBatchResults(batchResults [][]any, numTx int) ([][]byte, error) { 156 | var ( 157 | txHashes = make([][]byte, numTx) 158 | index = 0 159 | ) 160 | 161 | fmt.Printf("\nParsing batch results...\n") 162 | 163 | bar := progressbar.Default(int64(numTx), "results parsed") 164 | 165 | // Parsing is done in a separate loop to not hinder 166 | // the batch send speed (as txs need to be parsed sequentially) 167 | for _, batchResult := range batchResults { 168 | // For each batch, extract the transaction hashes 169 | for _, txResultRaw := range batchResult { 170 | txResult, ok := txResultRaw.(*core_types.ResultBroadcastTx) 171 | if !ok { 172 | return nil, errors.New("invalid result type returned") 173 | } 174 | 175 | // Check the errors 176 | if txResult.Error != nil { 177 | return nil, fmt.Errorf( 178 | "error when parsing transaction %s, %w", 179 | txResult.Hash, 180 | txResult.Error, 181 | ) 182 | } 183 | 184 | txHashes[index] = txResult.Hash 185 | index++ 186 | 187 | _ = bar.Add(1) //nolint:errcheck // No need to check 188 | } 189 | } 190 | 191 | fmt.Printf("✅ Successfully parsed %d batch results\n", len(batchResults)) 192 | 193 | return txHashes, nil 194 | } 195 | 196 | // generateBatches generates data batches based on passed in params 197 | func generateBatches(items [][]byte, batchSize int) [][][]byte { 198 | numBatches := int(math.Ceil(float64(len(items)) / float64(batchSize))) 199 | if numBatches == 0 { 200 | numBatches = 1 201 | } 202 | 203 | batches := make([][][]byte, numBatches) 204 | for i := 0; i < numBatches; i++ { 205 | batches[i] = make([][]byte, 0) 206 | } 207 | 208 | currentBatch := 0 209 | for _, item := range items { 210 | batches[currentBatch] = append(batches[currentBatch], item) 211 | 212 | if len(batches[currentBatch])%batchSize == 0 { 213 | currentBatch++ 214 | } 215 | } 216 | 217 | return batches 218 | } 219 | -------------------------------------------------------------------------------- /internal/batcher/batcher_test.go: -------------------------------------------------------------------------------- 1 | package batcher 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "fmt" 7 | "testing" 8 | 9 | core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 10 | "github.com/gnolang/gno/tm2/pkg/std" 11 | "github.com/gnolang/supernova/internal/common" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // generateRandomData generates random 32B chunks 16 | func generateRandomData(t *testing.T, count int) [][]byte { 17 | t.Helper() 18 | 19 | data := make([][]byte, count) 20 | 21 | for i := 0; i < count; i++ { 22 | buf := make([]byte, 32) 23 | 24 | _, err := rand.Read(buf) 25 | if err != nil { 26 | t.Fatalf("unable to generate random data, %v", err) 27 | } 28 | 29 | data[i] = buf 30 | } 31 | 32 | return data 33 | } 34 | 35 | // generateTestTransactions generates test transactions 36 | func generateTestTransactions(count int) []*std.Tx { 37 | data := make([]*std.Tx, count) 38 | 39 | for i := 0; i < count; i++ { 40 | data[i] = &std.Tx{ 41 | Memo: fmt.Sprintf("tx-%d", i), 42 | } 43 | } 44 | 45 | return data 46 | } 47 | 48 | func TestBatcher_BatchTransactions(t *testing.T) { 49 | t.Parallel() 50 | 51 | var ( 52 | numTxs = 100 53 | batchSize = 20 54 | txs = generateTestTransactions(numTxs) 55 | txHashes = generateRandomData(t, numTxs) 56 | 57 | broadcastTxs = make([][]byte, 0) 58 | currIndex = 0 59 | 60 | mockBatch = &mockBatch{ 61 | addTxBroadcastFn: func(tx []byte) error { 62 | broadcastTxs = append(broadcastTxs, tx) 63 | 64 | return nil 65 | }, 66 | executeFn: func() ([]interface{}, error) { 67 | res := make([]any, batchSize) 68 | 69 | for i := 0; i < batchSize; i++ { 70 | res[i] = &core_types.ResultBroadcastTx{ 71 | Hash: txHashes[currIndex], 72 | } 73 | 74 | currIndex++ 75 | } 76 | 77 | return res, nil 78 | }, 79 | } 80 | mockClient = &mockClient{ 81 | createBatchFn: func() common.Batch { 82 | return mockBatch 83 | }, 84 | } 85 | ) 86 | 87 | // Create the batcher 88 | b := NewBatcher(mockClient) 89 | 90 | // Batch the transactions 91 | res, err := b.BatchTransactions(txs, batchSize) 92 | if err != nil { 93 | t.Fatalf("unable to batch transactions, %v", err) 94 | } 95 | 96 | assert.NotNil(t, res) 97 | 98 | if len(res.TxHashes) != numTxs { 99 | t.Fatalf("invalid tx hashes returned, %d", len(res.TxHashes)) 100 | } 101 | 102 | for index, txHash := range txHashes { 103 | assert.True(t, bytes.Equal(txHash, txHashes[index])) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/batcher/mock_test.go: -------------------------------------------------------------------------------- 1 | package batcher 2 | 3 | import ( 4 | "github.com/gnolang/supernova/internal/common" 5 | ) 6 | 7 | type ( 8 | createBatchDelegate func() common.Batch 9 | getLatestBlockHeightDelegate func() (int64, error) 10 | ) 11 | 12 | type mockClient struct { 13 | createBatchFn createBatchDelegate 14 | getLatestBlockHeightFn getLatestBlockHeightDelegate 15 | } 16 | 17 | func (m *mockClient) CreateBatch() common.Batch { 18 | if m.createBatchFn != nil { 19 | return m.createBatchFn() 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func (m *mockClient) GetLatestBlockHeight() (int64, error) { 26 | if m.getLatestBlockHeightFn != nil { 27 | return m.getLatestBlockHeightFn() 28 | } 29 | 30 | return 0, nil 31 | } 32 | 33 | type ( 34 | addTxBroadcastDelegate func(tx []byte) error 35 | executeDelegate func() ([]interface{}, error) 36 | ) 37 | 38 | type mockBatch struct { 39 | addTxBroadcastFn addTxBroadcastDelegate 40 | executeFn executeDelegate 41 | } 42 | 43 | func (m *mockBatch) AddTxBroadcast(tx []byte) error { 44 | if m.addTxBroadcastFn != nil { 45 | return m.addTxBroadcastFn(tx) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (m *mockBatch) Execute() ([]interface{}, error) { 52 | if m.executeFn != nil { 53 | return m.executeFn() 54 | } 55 | 56 | return nil, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/batcher/types.go: -------------------------------------------------------------------------------- 1 | package batcher 2 | 3 | import ( 4 | "github.com/gnolang/supernova/internal/common" 5 | ) 6 | 7 | type Client interface { 8 | CreateBatch() common.Batch 9 | GetLatestBlockHeight() (int64, error) 10 | } 11 | 12 | // TxBatchResult contains batching results 13 | type TxBatchResult struct { 14 | TxHashes [][]byte // the tx hashes 15 | StartBlock int64 // the initial block for querying 16 | } 17 | -------------------------------------------------------------------------------- /internal/client/batch.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" 8 | ) 9 | 10 | type Batch struct { 11 | batch *client.RPCBatch 12 | } 13 | 14 | func (b *Batch) AddTxBroadcast(tx []byte) error { 15 | if err := b.batch.BroadcastTxSync(tx); err != nil { 16 | return fmt.Errorf("unable to prepare transaction, %w", err) 17 | } 18 | 19 | return nil 20 | } 21 | 22 | func (b *Batch) Execute() ([]interface{}, error) { 23 | return b.batch.Send(context.Background()) 24 | } 25 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gnolang/gno/gno.land/pkg/gnoland" 7 | "github.com/gnolang/gno/tm2/pkg/amino" 8 | abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" 9 | "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" 10 | core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 11 | "github.com/gnolang/gno/tm2/pkg/std" 12 | "github.com/gnolang/supernova/internal/common" 13 | ) 14 | 15 | const simulatePath = ".app/simulate" 16 | 17 | type Client struct { 18 | conn *client.RPCClient 19 | } 20 | 21 | // NewWSClient creates a new instance of the WS client 22 | func NewWSClient(url string) (*Client, error) { 23 | cli, err := client.NewWSClient(url) 24 | if err != nil { 25 | return nil, fmt.Errorf("unable to create ws client, %w", err) 26 | } 27 | 28 | return &Client{ 29 | conn: cli, 30 | }, nil 31 | } 32 | 33 | // NewHTTPClient creates a new instance of the HTTP client 34 | func NewHTTPClient(url string) (*Client, error) { 35 | cli, err := client.NewHTTPClient(url) 36 | if err != nil { 37 | return nil, fmt.Errorf("unable to create http client, %w", err) 38 | } 39 | 40 | return &Client{ 41 | conn: cli, 42 | }, nil 43 | } 44 | 45 | func (h *Client) CreateBatch() common.Batch { 46 | return &Batch{batch: h.conn.NewBatch()} 47 | } 48 | 49 | func (h *Client) ExecuteABCIQuery(path string, data []byte) (*core_types.ResultABCIQuery, error) { 50 | return h.conn.ABCIQuery(path, data) 51 | } 52 | 53 | func (h *Client) GetLatestBlockHeight() (int64, error) { 54 | status, err := h.conn.Status() 55 | if err != nil { 56 | return 0, fmt.Errorf("unable to fetch status, %w", err) 57 | } 58 | 59 | return status.SyncInfo.LatestBlockHeight, nil 60 | } 61 | 62 | func (h *Client) GetBlock(height *int64) (*core_types.ResultBlock, error) { 63 | return h.conn.Block(height) 64 | } 65 | 66 | func (h *Client) GetBlockResults(height *int64) (*core_types.ResultBlockResults, error) { 67 | return h.conn.BlockResults(height) 68 | } 69 | 70 | func (h *Client) GetConsensusParams(height *int64) (*core_types.ResultConsensusParams, error) { 71 | return h.conn.ConsensusParams(height) 72 | } 73 | 74 | func (h *Client) BroadcastTransaction(tx *std.Tx) error { 75 | marshalledTx, err := amino.Marshal(tx) 76 | if err != nil { 77 | return fmt.Errorf("unable to marshal transaction, %w", err) 78 | } 79 | 80 | res, err := h.conn.BroadcastTxCommit(marshalledTx) 81 | if err != nil { 82 | return fmt.Errorf("unable to broadcast transaction, %w", err) 83 | } 84 | 85 | if res.CheckTx.IsErr() { 86 | return fmt.Errorf("broadcast transaction check failed, %w", res.CheckTx.Error) 87 | } 88 | 89 | if res.DeliverTx.IsErr() { 90 | return fmt.Errorf("broadcast transaction delivery failed, %w", res.DeliverTx.Error) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (h *Client) GetAccount(address string) (*gnoland.GnoAccount, error) { 97 | queryResult, err := h.conn.ABCIQuery( 98 | fmt.Sprintf("auth/accounts/%s", address), 99 | []byte{}, 100 | ) 101 | if err != nil { 102 | return nil, fmt.Errorf("unable to fetch account %s, %w", address, err) 103 | } 104 | 105 | if queryResult.Response.IsErr() { 106 | return nil, fmt.Errorf("invalid account query result, %w", queryResult.Response.Error) 107 | } 108 | 109 | var acc gnoland.GnoAccount 110 | if err := amino.UnmarshalJSON(queryResult.Response.Data, &acc); err != nil { 111 | return nil, fmt.Errorf("unable to unmarshal query response, %w", err) 112 | } 113 | 114 | return &acc, nil 115 | } 116 | 117 | func (h *Client) GetBlockGasUsed(height int64) (int64, error) { 118 | blockRes, err := h.conn.BlockResults(&height) 119 | if err != nil { 120 | return 0, fmt.Errorf("unable to fetch block results, %w", err) 121 | } 122 | 123 | gasUsed := int64(0) 124 | for _, tx := range blockRes.Results.DeliverTxs { 125 | gasUsed += tx.GasUsed 126 | } 127 | 128 | return gasUsed, nil 129 | } 130 | 131 | func (h *Client) GetBlockGasLimit(height int64) (int64, error) { 132 | consensusParams, err := h.conn.ConsensusParams(&height) 133 | if err != nil { 134 | return 0, fmt.Errorf("unable to fetch block info, %w", err) 135 | } 136 | 137 | return consensusParams.ConsensusParams.Block.MaxGas, nil 138 | } 139 | 140 | func (h *Client) EstimateGas(tx *std.Tx) (int64, error) { 141 | // Prepare the transaction. 142 | // The transaction needs to be amino-binary encoded 143 | // in order to be estimated 144 | encodedTx, err := amino.Marshal(tx) 145 | if err != nil { 146 | return 0, fmt.Errorf("unable to marshal tx: %w", err) 147 | } 148 | 149 | // Perform the simulation query 150 | resp, err := h.conn.ABCIQuery(simulatePath, encodedTx) 151 | if err != nil { 152 | return 0, fmt.Errorf("unable to perform ABCI query: %w", err) 153 | } 154 | 155 | // Extract the query response 156 | if err = resp.Response.Error; err != nil { 157 | return 0, fmt.Errorf("error encountered during ABCI query: %w", err) 158 | } 159 | 160 | var deliverTx abci.ResponseDeliverTx 161 | if err = amino.Unmarshal(resp.Response.Value, &deliverTx); err != nil { 162 | return 0, fmt.Errorf("unable to unmarshal gas estimation response: %w", err) 163 | } 164 | 165 | if err = deliverTx.Error; err != nil { 166 | return 0, fmt.Errorf("error encountered during gas estimation: %w", err) 167 | } 168 | 169 | // Return the actual value returned by the node 170 | // for executing the transaction 171 | return deliverTx.GasUsed, nil 172 | } 173 | -------------------------------------------------------------------------------- /internal/collector/collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gnolang/gno/tm2/pkg/bft/types" 9 | "github.com/schollz/progressbar/v3" 10 | ) 11 | 12 | var errTimeout = errors.New("collector timed out") 13 | 14 | // Collector is the transaction / block stat 15 | // collector. 16 | // This implementation will heavily change when 17 | // transaction indexing is introduced 18 | type Collector struct { 19 | cli Client 20 | 21 | requestTimeout time.Duration 22 | } 23 | 24 | // NewCollector creates a new instance of the collector 25 | func NewCollector(cli Client) *Collector { 26 | return &Collector{ 27 | cli: cli, 28 | requestTimeout: time.Second * 2, 29 | } 30 | } 31 | 32 | // GetRunResult generates the run result for the passed in transaction hashes and start range 33 | func (c *Collector) GetRunResult( 34 | txHashes [][]byte, 35 | startBlock int64, 36 | startTime time.Time, 37 | ) (*RunResult, error) { 38 | var ( 39 | blockResults = make([]*BlockResult, 0) 40 | timeout = time.After(5 * time.Minute) 41 | start = startBlock 42 | txMap = newTxLookup(txHashes) 43 | processed = 0 44 | ) 45 | 46 | fmt.Printf("\n📊 Collecting Results 📊\n\n") 47 | 48 | bar := progressbar.Default(int64(len(txHashes)), "txs collected") 49 | 50 | for { 51 | // Check if all original transactions 52 | // were processed 53 | if processed >= len(txHashes) { 54 | break 55 | } 56 | 57 | select { 58 | case <-timeout: 59 | return nil, errTimeout 60 | case <-time.After(c.requestTimeout): 61 | latest, err := c.cli.GetLatestBlockHeight() 62 | if err != nil { 63 | return nil, fmt.Errorf("unable to fetch latest block height, %w", err) 64 | } 65 | 66 | if latest < start { 67 | // No need to parse older blocks 68 | continue 69 | } 70 | 71 | // Iterate over each block and find relevant transactions 72 | for blockNum := start; blockNum <= latest; blockNum++ { 73 | // Fetch the block 74 | block, err := c.cli.GetBlock(&blockNum) 75 | if err != nil { 76 | return nil, fmt.Errorf("unable to fetch block, %w", err) 77 | } 78 | 79 | // Check if any of the block transactions are the ones 80 | // sent out in the stress test 81 | belong := txMap.anyBelong(block.Block.Txs) 82 | if belong == 0 { 83 | continue 84 | } 85 | 86 | processed += belong 87 | _ = bar.Add(belong) //nolint:errcheck // No need to check 88 | 89 | // Fetch the total gas used by transactions 90 | blockGasUsed, err := c.cli.GetBlockGasUsed(blockNum) 91 | if err != nil { 92 | return nil, fmt.Errorf("unable to fetch block gas used, %w", err) 93 | } 94 | 95 | // Fetch the block gas limit 96 | blockGasLimit, err := c.cli.GetBlockGasLimit(blockNum) 97 | if err != nil { 98 | return nil, fmt.Errorf("unable to fetch block gas limit, %w", err) 99 | } 100 | 101 | blockResults = append(blockResults, &BlockResult{ 102 | Number: blockNum, 103 | Time: block.BlockMeta.Header.Time, 104 | Transactions: block.BlockMeta.Header.NumTxs, 105 | GasUsed: blockGasUsed, 106 | GasLimit: blockGasLimit, 107 | }) 108 | } 109 | 110 | // Update the iteration range 111 | start = latest + 1 112 | } 113 | } 114 | 115 | return &RunResult{ 116 | AverageTPS: calculateTPS( 117 | startTime, 118 | len(txHashes), 119 | ), 120 | Blocks: blockResults, 121 | }, nil 122 | } 123 | 124 | // txLookup is a simple lookup map for transaction hashes 125 | type txLookup struct { 126 | lookup map[string]struct{} 127 | } 128 | 129 | // newTxLookup creates a new instance of the tx lookup map 130 | func newTxLookup(txs [][]byte) *txLookup { 131 | lookup := make(map[string]struct{}) 132 | 133 | for _, tx := range txs { 134 | lookup[string(tx)] = struct{}{} 135 | } 136 | 137 | return &txLookup{ 138 | lookup: lookup, 139 | } 140 | } 141 | 142 | // anyBelong returns the number of transactions 143 | // that have been found in the lookup map 144 | func (t *txLookup) anyBelong(txs types.Txs) int { 145 | belong := 0 146 | 147 | for _, tx := range txs { 148 | txHash := tx.Hash() 149 | 150 | if _, ok := t.lookup[string(txHash)]; ok { 151 | belong++ 152 | } 153 | } 154 | 155 | return belong 156 | } 157 | 158 | // calculateTPS calculates the TPS for the sequence 159 | func calculateTPS(startTime time.Time, totalTx int) float64 { 160 | diff := time.Since(startTime).Seconds() 161 | 162 | return float64(totalTx) / diff 163 | } 164 | -------------------------------------------------------------------------------- /internal/collector/collector_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | "time" 7 | 8 | core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 9 | "github.com/gnolang/gno/tm2/pkg/bft/types" 10 | "github.com/gnolang/gno/tm2/pkg/crypto/tmhash" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // generateRandomData generates random 32B chunks 15 | func generateRandomData(t *testing.T, count int) [][]byte { 16 | t.Helper() 17 | 18 | data := make([][]byte, count) 19 | 20 | for i := 0; i < count; i++ { 21 | buf := make([]byte, 32) 22 | 23 | _, err := rand.Read(buf) 24 | if err != nil { 25 | t.Fatalf("unable to generate random data, %v", err) 26 | } 27 | 28 | data[i] = buf 29 | } 30 | 31 | return data 32 | } 33 | 34 | func TestCollector_GetRunResults(t *testing.T) { 35 | t.Parallel() 36 | 37 | numTxs := 100 38 | startTime := time.Now() 39 | blockTimes := make([]time.Time, numTxs) 40 | 41 | for i := 0; i < numTxs; i++ { 42 | if i == 0 { 43 | blockTimes[i] = startTime 44 | } 45 | 46 | blockTimes[i] = startTime.Add(time.Duration(i) * time.Second) 47 | } 48 | 49 | txs := generateRandomData(t, numTxs) 50 | txHashes := make([][]byte, numTxs) 51 | 52 | for i := 0; i < numTxs; i++ { 53 | txHashes[i] = tmhash.Sum(txs[i]) 54 | } 55 | 56 | var ( 57 | gasLimit = int64(1000) 58 | gasUsed = int64(100) 59 | 60 | mockClient = &mockClient{ 61 | getBlockFn: func(height *int64) (*core_types.ResultBlock, error) { 62 | if *height > int64(numTxs) { 63 | t.Fatalf("invalid height requested") 64 | } 65 | 66 | return &core_types.ResultBlock{ 67 | BlockMeta: &types.BlockMeta{ 68 | Header: types.Header{ 69 | Height: *height, 70 | Time: blockTimes[*height-1], 71 | NumTxs: 1, 72 | }, 73 | }, 74 | Block: &types.Block{ 75 | Data: types.Data{ 76 | Txs: []types.Tx{ 77 | txs[*height-1], 78 | }, 79 | }, 80 | }, 81 | }, nil 82 | }, 83 | getLatestBlockHeightFn: func() (int64, error) { 84 | return int64(numTxs), nil 85 | }, 86 | getBlockGasLimitFn: func(height int64) (int64, error) { 87 | if height > int64(numTxs) { 88 | t.Fatalf("invalid height requested") 89 | } 90 | 91 | return gasLimit, nil 92 | }, 93 | getBlockGasUsedFn: func(height int64) (int64, error) { 94 | if height > int64(numTxs) { 95 | t.Fatalf("invalid height requested") 96 | } 97 | 98 | return gasUsed, nil 99 | }, 100 | } 101 | ) 102 | 103 | // Create the collector 104 | c := NewCollector(mockClient) 105 | c.requestTimeout = time.Second * 0 106 | 107 | // Collect the results 108 | result, err := c.GetRunResult(txHashes, 1, startTime) 109 | if err != nil { 110 | t.Fatalf("unable to get run results, %v", err) 111 | } 112 | 113 | if result == nil { 114 | t.Fatal("result should not be nil") 115 | } 116 | 117 | assert.NotZero(t, result.AverageTPS) 118 | assert.Len(t, result.Blocks, numTxs) 119 | 120 | for index, block := range result.Blocks { 121 | assert.Equal(t, int64(index+1), block.Number) 122 | assert.Equal(t, gasUsed, block.GasUsed) 123 | assert.Equal(t, gasLimit, block.GasLimit) 124 | assert.Equal(t, int64(1), block.Transactions) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/collector/mock_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 4 | 5 | type ( 6 | getBlockDelegate func(height *int64) (*core_types.ResultBlock, error) 7 | getBlockGasUsedDelegate func(height int64) (int64, error) 8 | getBlockGasLimitDelegate func(height int64) (int64, error) 9 | getLatestBlockHeightDelegate func() (int64, error) 10 | ) 11 | 12 | type mockClient struct { 13 | getBlockFn getBlockDelegate 14 | getBlockGasUsedFn getBlockGasUsedDelegate 15 | getBlockGasLimitFn getBlockGasLimitDelegate 16 | getLatestBlockHeightFn getLatestBlockHeightDelegate 17 | } 18 | 19 | func (m *mockClient) GetBlock(height *int64) (*core_types.ResultBlock, error) { 20 | if m.getBlockFn != nil { 21 | return m.getBlockFn(height) 22 | } 23 | 24 | return nil, nil 25 | } 26 | 27 | func (m *mockClient) GetBlockGasUsed(height int64) (int64, error) { 28 | if m.getBlockGasUsedFn != nil { 29 | return m.getBlockGasUsedFn(height) 30 | } 31 | 32 | return 0, nil 33 | } 34 | 35 | func (m *mockClient) GetBlockGasLimit(height int64) (int64, error) { 36 | if m.getBlockGasLimitFn != nil { 37 | return m.getBlockGasLimitFn(height) 38 | } 39 | 40 | return 0, nil 41 | } 42 | 43 | func (m *mockClient) GetLatestBlockHeight() (int64, error) { 44 | if m.getLatestBlockHeightFn != nil { 45 | return m.getLatestBlockHeightFn() 46 | } 47 | 48 | return 0, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/collector/types.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "time" 5 | 6 | core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" 7 | ) 8 | 9 | type Client interface { 10 | GetBlock(height *int64) (*core_types.ResultBlock, error) 11 | GetBlockGasUsed(height int64) (int64, error) 12 | GetBlockGasLimit(height int64) (int64, error) 13 | GetLatestBlockHeight() (int64, error) 14 | } 15 | 16 | // RunResult is the complete test-run result 17 | type RunResult struct { 18 | Blocks []*BlockResult `json:"blocks"` 19 | AverageTPS float64 `json:"averageTPS"` 20 | } 21 | 22 | // BlockResult is the single-block test run result 23 | type BlockResult struct { 24 | Time time.Time `json:"created"` 25 | Number int64 `json:"blockNumber"` 26 | Transactions int64 `json:"numTransactions"` 27 | GasUsed int64 `json:"gasUsed"` 28 | GasLimit int64 `json:"gasLimit"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/gnolang/gno/tm2/pkg/std" 4 | 5 | const Denomination = "ugnot" 6 | 7 | // DefaultGasPrice represents the gno.land chain's 8 | // default minimum gas price ratio, which is 0.001ugnot/gas 9 | var DefaultGasPrice = std.GasPrice{ 10 | Gas: 1000, 11 | Price: std.Coin{ 12 | Denom: Denomination, 13 | Amount: 1, 14 | }, 15 | } 16 | 17 | // CalculateFeeInRatio calculates the minimum gas fee that should be specified 18 | // in a transaction, given the gas wanted (of the tx) and the reference gas ratio 19 | func CalculateFeeInRatio(gasWanted int64, reference std.GasPrice) std.Fee { 20 | // required amount = ceil((gas wanted * reference.Price.Amount) / reference.Gas) 21 | requiredAmount := (gasWanted*reference.Price.Amount + reference.Gas - 1) / reference.Gas 22 | 23 | return std.Fee{ 24 | GasWanted: gasWanted, 25 | GasFee: std.Coin{ 26 | Denom: reference.Price.Denom, 27 | Amount: requiredAmount, 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Batch is a common transaction batch 4 | type Batch interface { 5 | // AddTxBroadcast adds the transaction broadcast to the batch 6 | AddTxBroadcast(tx []byte) error 7 | 8 | // Execute executes the batch send 9 | Execute() ([]interface{}, error) 10 | } 11 | -------------------------------------------------------------------------------- /internal/config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | 7 | "github.com/gnolang/gno/tm2/pkg/crypto/bip39" 8 | "github.com/gnolang/supernova/internal/runtime" 9 | ) 10 | 11 | var ( 12 | errInvalidURL = errors.New("invalid node URL specified") 13 | errInvalidMnemonic = errors.New("invalid Mnemonic specified") 14 | errInvalidMode = errors.New("invalid mode specified") 15 | errInvalidSubaccounts = errors.New("invalid number of subaccounts specified") 16 | errInvalidTransactions = errors.New("invalid number of transactions specified") 17 | errInvalidBatchSize = errors.New("invalid batch size specified") 18 | ) 19 | 20 | var ( 21 | // httpRegex is used for verifying the cluster's JSON-RPC HTTP endpoint 22 | httpRegex = regexp.MustCompile(`(https?://.*)(:(\d*)/?(.*))?`) 23 | 24 | // wsRegex is used for verifying the cluster's JSON-RPC WS endpoint 25 | wsRegex = regexp.MustCompile(`(wss?://.*)(:(\d*)/?(.*))?`) 26 | ) 27 | 28 | // Config is the central pipeline configuration 29 | type Config struct { 30 | URL string // the URL of the cluster 31 | ChainID string // the chain ID of the cluster 32 | Mnemonic string // the mnemonic for the keyring 33 | Mode string // the stress test mode 34 | Output string // output path for results JSON, if any 35 | 36 | SubAccounts uint64 // the number of sub-accounts in the run 37 | Transactions uint64 // the total number of transactions 38 | BatchSize uint64 // the maximum size of the batch 39 | } 40 | 41 | // Validate validates the stress-test configuration 42 | func (cfg *Config) Validate() error { 43 | // Make sure the URL is valid 44 | if !httpRegex.MatchString(cfg.URL) && 45 | !wsRegex.MatchString(cfg.URL) { 46 | return errInvalidURL 47 | } 48 | 49 | // Make sure the mnemonic is valid 50 | if !bip39.IsMnemonicValid(cfg.Mnemonic) { 51 | return errInvalidMnemonic 52 | } 53 | 54 | // Make sure the mode is valid 55 | if !runtime.IsRuntime(runtime.Type(cfg.Mode)) { 56 | return errInvalidMode 57 | } 58 | 59 | // Make sure the number of subaccounts is valid 60 | if cfg.SubAccounts < 1 { 61 | return errInvalidSubaccounts 62 | } 63 | 64 | // Make sure the number of transactions is valid 65 | if cfg.Transactions < 1 { 66 | return errInvalidTransactions 67 | } 68 | 69 | // Make sure the batch size is valid 70 | if cfg.BatchSize < 1 { 71 | return errInvalidBatchSize 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/distributor/distributor.go: -------------------------------------------------------------------------------- 1 | package distributor 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/gnolang/gno/gno.land/pkg/gnoland" 9 | "github.com/gnolang/gno/tm2/pkg/crypto" 10 | "github.com/gnolang/gno/tm2/pkg/sdk/bank" 11 | "github.com/gnolang/gno/tm2/pkg/std" 12 | "github.com/gnolang/supernova/internal/common" 13 | "github.com/gnolang/supernova/internal/signer" 14 | "github.com/schollz/progressbar/v3" 15 | ) 16 | 17 | var errInsufficientFunds = errors.New("insufficient distributor funds") 18 | 19 | type Client interface { 20 | GetAccount(address string) (*gnoland.GnoAccount, error) 21 | BroadcastTransaction(tx *std.Tx) error 22 | EstimateGas(tx *std.Tx) (int64, error) 23 | } 24 | 25 | // Distributor is the process 26 | // that manages sub-account distributions 27 | type Distributor struct { 28 | cli Client 29 | } 30 | 31 | // NewDistributor creates a new instance of the distributor 32 | func NewDistributor( 33 | cli Client, 34 | ) *Distributor { 35 | return &Distributor{ 36 | cli: cli, 37 | } 38 | } 39 | 40 | // Distribute distributes the funds from the base account 41 | // (account 0 in the mnemonic) to other subaccounts 42 | func (d *Distributor) Distribute( 43 | distributor crypto.PrivKey, 44 | accounts []crypto.Address, 45 | transactions uint64, 46 | chainID string, 47 | ) ([]std.Account, error) { 48 | fmt.Printf("\n💸 Starting Fund Distribution 💸\n\n") 49 | 50 | // Calculate the base fees 51 | subAccountCost := calculateRuntimeCosts(int64(transactions)) 52 | fmt.Printf( 53 | "Calculated sub-account cost as %d %s\n", 54 | subAccountCost.Amount, 55 | subAccountCost.Denom, 56 | ) 57 | 58 | // Fund the accounts 59 | return d.fundAccounts(distributor, accounts, subAccountCost, chainID) 60 | } 61 | 62 | // calculateRuntimeCosts calculates the amount of funds 63 | // each account needs to have in order to participate in the 64 | // stress test run 65 | func calculateRuntimeCosts(totalTx int64) std.Coin { 66 | // Cost of a single run transaction for the sub-account 67 | // NOTE: Since there is no gas estimation support yet, this value 68 | // is fixed, but it will change in the future once pricing estimations 69 | // are added 70 | baseTxCost := common.CalculateFeeInRatio(1_000_000, common.DefaultGasPrice) 71 | 72 | // Each account should have enough funds 73 | // to execute the entire run 74 | subAccountCost := std.Coin{ 75 | Denom: common.Denomination, 76 | Amount: totalTx * baseTxCost.GasFee.Amount, 77 | } 78 | 79 | return subAccountCost 80 | } 81 | 82 | // fundAccounts attempts to fund accounts that have missing funds, 83 | // and returns the accounts that can participate in the stress test 84 | func (d *Distributor) fundAccounts( 85 | distributorKey crypto.PrivKey, 86 | accounts []crypto.Address, 87 | singleRunCost std.Coin, 88 | chainID string, 89 | ) ([]std.Account, error) { 90 | type shortAccount struct { 91 | missingFunds std.Coin 92 | address crypto.Address 93 | } 94 | 95 | var ( 96 | // Accounts that are ready (funded) for the run 97 | readyAccounts = make([]std.Account, 0, len(accounts)) 98 | 99 | // Accounts that need funding 100 | shortAccounts = make([]shortAccount, 0, len(accounts)) 101 | ) 102 | 103 | // Check if there are any accounts that need to be funded 104 | // before the stress test starts 105 | for _, account := range accounts { 106 | // Fetch the account balance 107 | subAccount, err := d.cli.GetAccount(account.String()) 108 | if err != nil { 109 | return nil, fmt.Errorf("unable to fetch sub-account, %w", err) 110 | } 111 | 112 | // Check if it has enough funds for the run 113 | if subAccount.Coins.AmountOf(common.Denomination) < singleRunCost.Amount { 114 | // Mark the account as needing a top-up 115 | shortAccounts = append(shortAccounts, shortAccount{ 116 | address: account, 117 | missingFunds: std.Coin{ 118 | Denom: common.Denomination, 119 | Amount: singleRunCost.Amount - subAccount.Coins.AmountOf(common.Denomination), 120 | }, 121 | }) 122 | 123 | continue 124 | } 125 | 126 | // The account is cleared for the stress test 127 | readyAccounts = append(readyAccounts, subAccount) 128 | } 129 | 130 | // Check if funding is even necessary 131 | if len(shortAccounts) == 0 { 132 | // All accounts are already funded 133 | fmt.Printf("✅ All %d accounts are already funded\n", len(readyAccounts)) 134 | 135 | return readyAccounts, nil 136 | } 137 | 138 | // Sort the short accounts so the ones with 139 | // the lowest missing funds are funded first 140 | sort.Slice(shortAccounts, func(i, j int) bool { 141 | return shortAccounts[i].missingFunds.IsLT(shortAccounts[j].missingFunds) 142 | }) 143 | 144 | // Figure out how many accounts can actually be funded 145 | distributor, err := d.cli.GetAccount(distributorKey.PubKey().Address().String()) 146 | if err != nil { 147 | return nil, fmt.Errorf("unable to fetch distributor account, %w", err) 148 | } 149 | 150 | var ( 151 | distributorBalance = distributor.Coins 152 | fundableIndex = 0 153 | defaultFee = common.CalculateFeeInRatio(100_000, common.DefaultGasPrice) 154 | ) 155 | 156 | for _, account := range shortAccounts { 157 | // The transfer cost is the single run cost (missing balance) + approximate transfer cost 158 | transferCost := std.NewCoins(defaultFee.GasFee.Add(account.missingFunds)) 159 | 160 | if distributorBalance.IsAllLT(transferCost) { 161 | // Distributor does not have any more funds 162 | // to cover the run cost 163 | break 164 | } 165 | 166 | fundableIndex++ 167 | 168 | distributorBalance.Sub(transferCost) 169 | } 170 | 171 | if fundableIndex == 0 { 172 | // The distributor does not have funds to fund 173 | // any account for the stress test 174 | fmt.Printf( 175 | "❌ Distributor cannot fund any account, balance is %d %s\n", 176 | distributorBalance.AmountOf(common.Denomination), 177 | common.Denomination, 178 | ) 179 | 180 | return nil, errInsufficientFunds 181 | } 182 | 183 | // Locally keep track of the nonce, so 184 | // there is no need to re-fetch the account again 185 | // before signing a future tx 186 | nonce := distributor.Sequence 187 | 188 | fmt.Printf("Funding %d accounts...\n", len(shortAccounts)) 189 | bar := progressbar.Default(int64(len(shortAccounts)), "funding short accounts") 190 | 191 | for _, account := range shortAccounts { 192 | // Generate the transaction 193 | tx := &std.Tx{ 194 | Msgs: []std.Msg{ 195 | bank.MsgSend{ 196 | FromAddress: distributor.GetAddress(), 197 | ToAddress: account.address, 198 | Amount: std.NewCoins(account.missingFunds), 199 | }, 200 | }, 201 | Fee: defaultFee, 202 | } 203 | 204 | cfg := signer.SignCfg{ 205 | ChainID: chainID, 206 | AccountNumber: distributor.AccountNumber, 207 | Sequence: nonce, 208 | } 209 | 210 | // Sign the transaction 211 | if err := signer.SignTx(tx, distributorKey, cfg); err != nil { 212 | return nil, fmt.Errorf("unable to sign transaction, %w", err) 213 | } 214 | 215 | // Update the local nonce 216 | nonce++ 217 | 218 | // Broadcast the tx and wait for it to be committed 219 | if err := d.cli.BroadcastTransaction(tx); err != nil { 220 | return nil, fmt.Errorf("unable to broadcast tx with commit, %w", err) 221 | } 222 | 223 | // Since accounts can be uninitialized on the node, after the 224 | // transfer they will have acquired a storage slot, and need 225 | // to be re-fetched for their data (Sequence + Account Number) 226 | nodeAccount, err := d.cli.GetAccount(account.address.String()) 227 | if err != nil { 228 | return nil, fmt.Errorf("unable to fetch account, %w", err) 229 | } 230 | 231 | // Mark the account as funded 232 | readyAccounts = append(readyAccounts, nodeAccount) 233 | 234 | _ = bar.Add(1) //nolint:errcheck // No need to check 235 | } 236 | 237 | fmt.Printf("✅ Successfully funded %d accounts\n", len(shortAccounts)) 238 | 239 | return readyAccounts, nil 240 | } 241 | -------------------------------------------------------------------------------- /internal/distributor/distributor_test.go: -------------------------------------------------------------------------------- 1 | package distributor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gnolang/gno/gno.land/pkg/gnoland" 7 | "github.com/gnolang/gno/tm2/pkg/crypto" 8 | "github.com/gnolang/gno/tm2/pkg/sdk/bank" 9 | "github.com/gnolang/gno/tm2/pkg/std" 10 | "github.com/gnolang/supernova/internal/common" 11 | testutils "github.com/gnolang/supernova/internal/testing" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestDistributor_Distribute(t *testing.T) { 16 | t.Parallel() 17 | 18 | var ( 19 | numTx = uint64(1000) 20 | singleCost = calculateRuntimeCosts(int64(numTx)) 21 | ) 22 | 23 | getAccount := func(address string, accounts []crypto.PrivKey) crypto.PrivKey { 24 | for _, account := range accounts { 25 | if address == account.PubKey().Address().String() { 26 | return account 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | t.Run("all accounts funded", func(t *testing.T) { 34 | t.Parallel() 35 | 36 | var ( 37 | accounts = testutils.GenerateAccounts(t, 10) 38 | 39 | mockClient = &mockClient{ 40 | getAccountFn: func(address string) (*gnoland.GnoAccount, error) { 41 | acc := getAccount(address, accounts) 42 | if acc == nil { 43 | t.Fatal("invalid account requested") 44 | } 45 | 46 | return &gnoland.GnoAccount{ 47 | BaseAccount: *std.NewBaseAccount( 48 | acc.PubKey().Address(), 49 | std.NewCoins(singleCost), 50 | nil, 51 | 0, 52 | 0, 53 | ), 54 | }, nil 55 | }, 56 | } 57 | ) 58 | 59 | d := NewDistributor( 60 | mockClient, 61 | ) 62 | 63 | // Extract the addresses 64 | addresses := make([]crypto.Address, 0, len(accounts[1:])) 65 | for _, account := range accounts[1:] { 66 | addresses = append(addresses, account.PubKey().Address()) 67 | } 68 | 69 | readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") 70 | if err != nil { 71 | t.Fatalf("unable to distribute funds, %v", err) 72 | } 73 | 74 | // Make sure all accounts are funded 75 | // (the distributor does not participate in the run, hence -1) 76 | assert.Len(t, readyAccounts, len(accounts)-1) 77 | 78 | // Make sure the accounts match 79 | for index, account := range accounts[1:] { 80 | assert.Equal(t, account.PubKey().Address().String(), readyAccounts[index].GetAddress().String()) 81 | } 82 | }) 83 | 84 | t.Run("insufficient distributor funds", func(t *testing.T) { 85 | t.Parallel() 86 | 87 | emptyBalance := std.Coin{ 88 | Denom: common.Denomination, 89 | Amount: 0, 90 | } 91 | 92 | var ( 93 | accounts = testutils.GenerateAccounts(t, 10) 94 | 95 | mockClient = &mockClient{ 96 | getAccountFn: func(address string) (*gnoland.GnoAccount, error) { 97 | acc := getAccount(address, accounts) 98 | if acc == nil { 99 | t.Fatal("invalid account requested") 100 | } 101 | 102 | return &gnoland.GnoAccount{ 103 | BaseAccount: *std.NewBaseAccount( 104 | acc.PubKey().Address(), 105 | std.NewCoins(emptyBalance), 106 | nil, 107 | 0, 108 | 0, 109 | ), 110 | }, nil 111 | }, 112 | } 113 | ) 114 | 115 | d := NewDistributor( 116 | mockClient, 117 | ) 118 | 119 | // Extract the addresses 120 | addresses := make([]crypto.Address, 0, len(accounts[1:])) 121 | for _, account := range accounts[1:] { 122 | addresses = append(addresses, account.PubKey().Address()) 123 | } 124 | 125 | readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") 126 | 127 | assert.Nil(t, readyAccounts) 128 | assert.ErrorIs(t, err, errInsufficientFunds) 129 | }) 130 | 131 | t.Run("fund all short accounts", func(t *testing.T) { 132 | t.Parallel() 133 | 134 | emptyBalance := std.Coin{ 135 | Denom: common.Denomination, 136 | Amount: 0, 137 | } 138 | 139 | var ( 140 | accounts = testutils.GenerateAccounts(t, 10) 141 | capturedBroadcasts = make([]*std.Tx, 0) 142 | 143 | mockClient = &mockClient{ 144 | getAccountFn: func(address string) (*gnoland.GnoAccount, error) { 145 | acc := getAccount(address, accounts) 146 | if acc == nil { 147 | t.Fatal("invalid account requested") 148 | } 149 | 150 | if acc.Equals(accounts[0]) { 151 | sendCost := common.CalculateFeeInRatio(100_000, common.DefaultGasPrice) 152 | 153 | return &gnoland.GnoAccount{ 154 | BaseAccount: *std.NewBaseAccount( 155 | acc.PubKey().Address(), 156 | std.NewCoins(std.Coin{ 157 | Denom: common.Denomination, 158 | Amount: int64(numTx) * sendCost.GasFee.Add(singleCost).Amount, 159 | }), 160 | nil, 161 | 0, 162 | 0, 163 | ), 164 | }, nil 165 | } 166 | 167 | return &gnoland.GnoAccount{ 168 | BaseAccount: *std.NewBaseAccount( 169 | acc.PubKey().Address(), 170 | std.NewCoins(emptyBalance), 171 | nil, 172 | 0, 173 | 0, 174 | ), 175 | }, nil 176 | }, 177 | broadcastTransactionFn: func(tx *std.Tx) error { 178 | capturedBroadcasts = append(capturedBroadcasts, tx) 179 | 180 | return nil 181 | }, 182 | } 183 | ) 184 | 185 | d := NewDistributor( 186 | mockClient, 187 | ) 188 | 189 | // Extract the addresses 190 | addresses := make([]crypto.Address, 0, len(accounts[1:])) 191 | for _, account := range accounts[1:] { 192 | addresses = append(addresses, account.PubKey().Address()) 193 | } 194 | 195 | readyAccounts, err := d.Distribute(accounts[0], addresses, numTx, "dummy") 196 | if err != nil { 197 | t.Fatalf("unable to distribute funds, %v", err) 198 | } 199 | 200 | // Make sure all accounts are funded 201 | // (the distributor does not participate in the run, hence -1) 202 | assert.Len(t, readyAccounts, len(accounts)-1) 203 | 204 | // Make sure the accounts match 205 | for index, account := range accounts[1:] { 206 | assert.Equal(t, account.PubKey().Address(), readyAccounts[index].GetAddress()) 207 | } 208 | 209 | // Check the broadcast transactions 210 | assert.Len(t, capturedBroadcasts, len(accounts)-1) 211 | 212 | sendType := bank.MsgSend{}.Type() 213 | 214 | for _, tx := range capturedBroadcasts { 215 | if len(tx.Msgs) != 1 { 216 | t.Fatal("invalid number of messages") 217 | } 218 | 219 | assert.Equal(t, sendType, tx.Msgs[0].Type()) 220 | } 221 | }) 222 | } 223 | -------------------------------------------------------------------------------- /internal/distributor/mock_test.go: -------------------------------------------------------------------------------- 1 | package distributor 2 | 3 | import ( 4 | "github.com/gnolang/gno/gno.land/pkg/gnoland" 5 | "github.com/gnolang/gno/tm2/pkg/std" 6 | ) 7 | 8 | type ( 9 | broadcastTransactionDelegate func(*std.Tx) error 10 | getAccountDelegate func(string) (*gnoland.GnoAccount, error) 11 | estimateGasDelegate func(*std.Tx) (int64, error) 12 | ) 13 | 14 | type mockClient struct { 15 | broadcastTransactionFn broadcastTransactionDelegate 16 | getAccountFn getAccountDelegate 17 | estimateGasFn estimateGasDelegate 18 | } 19 | 20 | func (m *mockClient) BroadcastTransaction(tx *std.Tx) error { 21 | if m.broadcastTransactionFn != nil { 22 | return m.broadcastTransactionFn(tx) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (m *mockClient) GetAccount(address string) (*gnoland.GnoAccount, error) { 29 | if m.getAccountFn != nil { 30 | return m.getAccountFn(address) 31 | } 32 | 33 | return nil, nil 34 | } 35 | 36 | func (m *mockClient) EstimateGas(tx *std.Tx) (int64, error) { 37 | if m.estimateGasFn != nil { 38 | return m.estimateGasFn(tx) 39 | } 40 | 41 | return 0, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/output.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "text/tabwriter" 8 | 9 | "github.com/gnolang/supernova/internal/collector" 10 | ) 11 | 12 | // displayResults displays the runtime result in the terminal 13 | func displayResults(result *collector.RunResult) { 14 | w := tabwriter.NewWriter(os.Stdout, 10, 20, 2, ' ', 0) 15 | 16 | // TPS // 17 | _, _ = fmt.Fprintf(w, "\nTPS: %.2f\n", result.AverageTPS) 18 | 19 | // Block info // 20 | _, _ = fmt.Fprintln(w, "\nBlock #\tGas Used\tGas Limit\tTransactions\tUtilization") 21 | for _, block := range result.Blocks { 22 | _, _ = fmt.Fprintf( 23 | w, 24 | "Block #%d\t%d\t%d\t%d\t%.2f%%\n", 25 | block.Number, 26 | block.GasUsed, 27 | block.GasLimit, 28 | block.Transactions, 29 | (float64(block.GasUsed)/float64(block.GasLimit))*100, 30 | ) 31 | } 32 | 33 | _, _ = fmt.Fprintln(w, "") 34 | 35 | _ = w.Flush() 36 | } 37 | 38 | // saveResults saves the runtime results to a file 39 | func saveResults(result *collector.RunResult, path string) error { 40 | // Marshal the results 41 | resultJSON, err := json.Marshal(result) 42 | if err != nil { 43 | return fmt.Errorf("unable to marshal result, %w", err) 44 | } 45 | 46 | // Create the file 47 | f, err := os.Create(path) 48 | if err != nil { 49 | return fmt.Errorf("unable to create file, %w", err) 50 | } 51 | 52 | defer func() { 53 | _ = f.Close() 54 | }() 55 | 56 | // Write to file 57 | _, err = f.Write(resultJSON) 58 | if err != nil { 59 | return fmt.Errorf("unable to write to file, %w", err) 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/pipeline.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gnolang/gno/tm2/pkg/crypto" 8 | "github.com/gnolang/gno/tm2/pkg/crypto/bip39" 9 | "github.com/gnolang/supernova/internal/batcher" 10 | "github.com/gnolang/supernova/internal/client" 11 | "github.com/gnolang/supernova/internal/collector" 12 | "github.com/gnolang/supernova/internal/distributor" 13 | "github.com/gnolang/supernova/internal/runtime" 14 | "github.com/gnolang/supernova/internal/signer" 15 | "github.com/schollz/progressbar/v3" 16 | ) 17 | 18 | type pipelineClient interface { 19 | distributor.Client 20 | batcher.Client 21 | collector.Client 22 | } 23 | 24 | // Pipeline is the central run point 25 | // for the stress test 26 | type Pipeline struct { 27 | cfg *Config // the run configuration 28 | cli pipelineClient // HTTP client connection 29 | } 30 | 31 | // NewPipeline creates a new pipeline instance 32 | func NewPipeline(cfg *Config) (*Pipeline, error) { 33 | var ( 34 | cli *client.Client 35 | err error 36 | ) 37 | 38 | // Check which kind of client to create 39 | if httpRegex.MatchString(cfg.URL) { 40 | cli, err = client.NewHTTPClient(cfg.URL) 41 | } else { 42 | cli, err = client.NewWSClient(cfg.URL) 43 | } 44 | 45 | if err != nil { 46 | return nil, fmt.Errorf("unable to create RPC client, %w", err) 47 | } 48 | 49 | return &Pipeline{ 50 | cfg: cfg, 51 | cli: cli, 52 | }, nil 53 | } 54 | 55 | // Execute runs the entire pipeline process 56 | func (p *Pipeline) Execute() error { 57 | var ( 58 | mode = runtime.Type(p.cfg.Mode) 59 | 60 | txBatcher = batcher.NewBatcher(p.cli) 61 | txCollector = collector.NewCollector(p.cli) 62 | txRuntime = runtime.GetRuntime(mode) 63 | ) 64 | 65 | // Initialize the accounts for the runtime 66 | accounts := p.initializeAccounts() 67 | 68 | // Predeploy any pending transactions 69 | if err := prepareRuntime( 70 | mode, 71 | accounts[0], 72 | p.cfg.ChainID, 73 | p.cli, 74 | txRuntime, 75 | ); err != nil { 76 | return err 77 | } 78 | 79 | // Extract the addresses 80 | addresses := make([]crypto.Address, 0, len(accounts[1:])) 81 | for _, account := range accounts[1:] { 82 | addresses = append(addresses, account.PubKey().Address()) 83 | } 84 | 85 | // Distribute the funds to sub-accounts 86 | runAccounts, err := distributor.NewDistributor(p.cli).Distribute( 87 | accounts[0], 88 | addresses, 89 | p.cfg.Transactions, 90 | p.cfg.ChainID, 91 | ) 92 | if err != nil { 93 | return fmt.Errorf("unable to distribute funds, %w", err) 94 | } 95 | 96 | // Find which keys belong to the run accounts (not all initial accounts are run accounts) 97 | runKeys := make([]crypto.PrivKey, 0, len(runAccounts)) 98 | 99 | for _, runAccount := range runAccounts { 100 | for _, account := range accounts[1:] { 101 | if account.PubKey().Address() == runAccount.GetAddress() { 102 | runKeys = append(runKeys, account) 103 | } 104 | } 105 | } 106 | 107 | // Construct the transactions using the runtime 108 | txs, err := txRuntime.ConstructTransactions( 109 | runKeys, 110 | runAccounts, 111 | p.cfg.Transactions, 112 | p.cfg.ChainID, 113 | p.cli.EstimateGas, 114 | ) 115 | if err != nil { 116 | return fmt.Errorf("unable to construct transactions, %w", err) 117 | } 118 | 119 | // Send the signed transactions in batches 120 | batchStart := time.Now() 121 | 122 | batchResult, err := txBatcher.BatchTransactions(txs, int(p.cfg.BatchSize)) 123 | if err != nil { 124 | return fmt.Errorf("unable to batch transactions %w", err) 125 | } 126 | 127 | // Collect the transaction results 128 | runResult, err := txCollector.GetRunResult( 129 | batchResult.TxHashes, 130 | batchResult.StartBlock, 131 | batchStart, 132 | ) 133 | if err != nil { 134 | return fmt.Errorf("unable to collect transactions, %w", err) 135 | } 136 | 137 | // Display [+ save the results] 138 | return p.handleResults(runResult) 139 | } 140 | 141 | // initializeAccounts initializes the accounts needed for the stress test run 142 | func (p *Pipeline) initializeAccounts() []crypto.PrivKey { 143 | fmt.Printf("\n🧮 Initializing Accounts 🧮\n\n") 144 | fmt.Printf("Generating sub-accounts...\n") 145 | 146 | var ( 147 | accounts = make([]crypto.PrivKey, p.cfg.SubAccounts+1) 148 | bar = progressbar.Default(int64(p.cfg.SubAccounts+1), "accounts initialized") 149 | 150 | seed = bip39.NewSeed(p.cfg.Mnemonic, "") 151 | ) 152 | 153 | // Register the accounts with the keybase 154 | for i := 0; i < int(p.cfg.SubAccounts)+1; i++ { 155 | accounts[i] = signer.GenerateKeyFromSeed(seed, uint32(i)) 156 | _ = bar.Add(1) //nolint:errcheck // No need to check 157 | } 158 | 159 | fmt.Printf("✅ Successfully generated %d accounts\n", len(accounts)) 160 | 161 | return accounts 162 | } 163 | 164 | // handleResults displays the results in the terminal, 165 | // and saves them to disk if an output path was specified 166 | func (p *Pipeline) handleResults(runResult *collector.RunResult) error { 167 | // Display the results in the terminal 168 | displayResults(runResult) 169 | 170 | // Check if the results need to be saved to disk 171 | if p.cfg.Output == "" { 172 | // No disk save necessary 173 | return nil 174 | } 175 | 176 | fmt.Printf("\n💾 Saving Results 💾\n\n") 177 | 178 | if err := saveResults(runResult, p.cfg.Output); err != nil { 179 | return fmt.Errorf("unable to save results, %w", err) 180 | } 181 | 182 | fmt.Printf("✅ Successfully saved results to %s\n", p.cfg.Output) 183 | 184 | return nil 185 | } 186 | 187 | // prepareRuntime prepares the runtime by pre-deploying 188 | // any pending transactions 189 | func prepareRuntime( 190 | mode runtime.Type, 191 | deployerKey crypto.PrivKey, 192 | chainID string, 193 | cli pipelineClient, 194 | txRuntime runtime.Runtime, 195 | ) error { 196 | if mode != runtime.RealmCall { 197 | return nil 198 | } 199 | 200 | fmt.Printf("\n✨ Starting Predeployment Procedure ✨\n\n") 201 | 202 | // Get the deployer account 203 | deployer, err := cli.GetAccount(deployerKey.PubKey().Address().String()) 204 | if err != nil { 205 | return fmt.Errorf("unable to fetch deployer account, %w", err) 206 | } 207 | 208 | // Get the predeploy transactions 209 | predeployTxs, err := txRuntime.Initialize( 210 | deployer, 211 | deployerKey, 212 | chainID, 213 | cli.EstimateGas, 214 | ) 215 | if err != nil { 216 | return fmt.Errorf("unable to initialize runtime, %w", err) 217 | } 218 | 219 | bar := progressbar.Default(int64(len(predeployTxs)), "predeployed txs") 220 | 221 | // Execute the predeploy transactions 222 | for _, tx := range predeployTxs { 223 | if err := cli.BroadcastTransaction(tx); err != nil { 224 | return fmt.Errorf("unable to broadcast predeploy tx, %w", err) 225 | } 226 | 227 | _ = bar.Add(1) //nolint:errcheck // No need to check 228 | } 229 | 230 | fmt.Printf("✅ Successfully predeployed %d transactions\n", len(predeployTxs)) 231 | 232 | return nil 233 | } 234 | -------------------------------------------------------------------------------- /internal/runtime/helper.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gnolang/gno/tm2/pkg/crypto" 7 | "github.com/gnolang/gno/tm2/pkg/std" 8 | "github.com/gnolang/supernova/internal/common" 9 | "github.com/gnolang/supernova/internal/signer" 10 | "github.com/schollz/progressbar/v3" 11 | ) 12 | 13 | const gasBuffer = 10_000 // 10k gas 14 | 15 | // msgFn defines the transaction message constructor 16 | type msgFn func(creator std.Account, index int) std.Msg 17 | 18 | // constructTransactions constructs and signs the transactions 19 | // using the passed in message generator and signer 20 | func constructTransactions( 21 | keys []crypto.PrivKey, 22 | accounts []std.Account, 23 | transactions uint64, 24 | chainID string, 25 | getMsg msgFn, 26 | estimateFn EstimateGasFn, 27 | ) ([]*std.Tx, error) { 28 | var ( 29 | txs = make([]*std.Tx, transactions) 30 | 31 | // A local nonce map is updated to avoid unnecessary calls 32 | // for fetching the fresh info from the chain every time 33 | // an account is used 34 | nonceMap = make(map[uint64]uint64) // accountNumber -> nonce 35 | ) 36 | 37 | fmt.Printf("\n⏳ Estimating Gas ⏳\n") 38 | 39 | // Estimate the fee for the transaction batch 40 | txFee := common.CalculateFeeInRatio( 41 | 1_000_000, 42 | common.DefaultGasPrice, 43 | ) 44 | 45 | // Construct the first tx 46 | var ( 47 | creator = accounts[0] 48 | creatorKey = keys[0] 49 | ) 50 | 51 | tx := &std.Tx{ 52 | Msgs: []std.Msg{getMsg(creator, 0)}, 53 | Fee: txFee, 54 | } 55 | 56 | // Sign the transaction 57 | cfg := signer.SignCfg{ 58 | ChainID: chainID, 59 | AccountNumber: creator.GetAccountNumber(), 60 | Sequence: creator.GetSequence(), 61 | } 62 | 63 | if err := signer.SignTx(tx, creatorKey, cfg); err != nil { 64 | return nil, fmt.Errorf("unable to sign transaction, %w", err) 65 | } 66 | 67 | gasWanted, err := estimateFn(tx) 68 | if err != nil { 69 | return nil, fmt.Errorf("unable to estimate gas, %w", err) 70 | } 71 | 72 | // Clear the old signatures, because they need 73 | // to be regenerated 74 | clear(tx.Signatures) 75 | 76 | // Use the estimated gas limit 77 | txFee = common.CalculateFeeInRatio(gasWanted+gasBuffer, common.DefaultGasPrice) // 10k gas buffer 78 | 79 | if err = signer.SignTx(tx, creatorKey, cfg); err != nil { 80 | return nil, fmt.Errorf("unable to sign transaction, %w", err) 81 | } 82 | 83 | fmt.Printf("\nEstimated Gas for 1 run tx: %d \n", tx.Fee.GasWanted) 84 | fmt.Printf("\n🔨 Constructing Transactions 🔨\n\n") 85 | 86 | bar := progressbar.Default(int64(transactions), "constructing txs") 87 | 88 | for i := 0; i < int(transactions); i++ { 89 | // Generate the transaction 90 | var ( 91 | creator = accounts[i%len(accounts)] 92 | creatorKey = keys[i%len(accounts)] 93 | accountNumber = creator.GetAccountNumber() 94 | ) 95 | 96 | tx := &std.Tx{ 97 | Msgs: []std.Msg{getMsg(creator, i)}, 98 | Fee: txFee, 99 | } 100 | 101 | // Fetch the next account nonce 102 | nonce, found := nonceMap[creator.GetAccountNumber()] 103 | if !found { 104 | nonce = creator.GetSequence() 105 | nonceMap[creator.GetAccountNumber()] = nonce 106 | } 107 | 108 | // Sign the transaction 109 | cfg := signer.SignCfg{ 110 | ChainID: chainID, 111 | AccountNumber: accountNumber, 112 | Sequence: nonce, 113 | } 114 | 115 | if err := signer.SignTx(tx, creatorKey, cfg); err != nil { 116 | return nil, fmt.Errorf("unable to sign transaction, %w", err) 117 | } 118 | 119 | // Increase the creator nonce locally 120 | nonceMap[accountNumber] = nonce + 1 121 | 122 | // Mark the transaction as ready 123 | txs[i] = tx 124 | _ = bar.Add(1) //nolint:errcheck // No need to check 125 | } 126 | 127 | fmt.Printf("✅ Successfully constructed %d transactions\n", transactions) 128 | 129 | return txs, nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/runtime/helper_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gnolang/gno/gno.land/pkg/gnoland" 7 | "github.com/gnolang/gno/gno.land/pkg/sdk/vm" 8 | "github.com/gnolang/gno/tm2/pkg/std" 9 | "github.com/gnolang/supernova/internal/common" 10 | testutils "github.com/gnolang/supernova/internal/testing" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | // generateAccounts generates mock gno accounts 16 | func generateAccounts(count int) []std.Account { 17 | accounts := make([]std.Account, count) 18 | 19 | for i := 0; i < count; i++ { 20 | accounts[i] = &gnoland.GnoAccount{ 21 | BaseAccount: std.BaseAccount{ 22 | AccountNumber: uint64(i), 23 | }, 24 | } 25 | } 26 | 27 | return accounts 28 | } 29 | 30 | func TestHelper_ConstructTransactions(t *testing.T) { 31 | t.Parallel() 32 | 33 | var ( 34 | accounts = generateAccounts(10) 35 | accountKeys = testutils.GenerateAccounts(t, 10) 36 | nonceMap = make(map[uint64]uint64, len(accounts)) 37 | ) 38 | 39 | // Initialize the nonce map 40 | for _, account := range accounts { 41 | nonceMap[account.GetAccountNumber()] = 0 42 | } 43 | 44 | var ( 45 | transactions = uint64(100) 46 | msg = vm.MsgAddPackage{} 47 | 48 | getMsgFn = func(_ std.Account, _ int) std.Msg { 49 | return msg 50 | } 51 | ) 52 | 53 | txs, err := constructTransactions( 54 | accountKeys, 55 | accounts, 56 | transactions, 57 | "dummy", 58 | getMsgFn, 59 | func(_ *std.Tx) (int64, error) { 60 | return 1_000_000, nil 61 | }, 62 | ) 63 | require.NoError(t, err) 64 | 65 | assert.Len(t, txs, int(transactions)) 66 | 67 | // Make sure the constructed transactions are valid 68 | for _, tx := range txs { 69 | // Make sure the fee is valid 70 | assert.Equal( 71 | t, 72 | common.CalculateFeeInRatio(1_000_000+gasBuffer, common.DefaultGasPrice), 73 | tx.Fee, 74 | ) 75 | 76 | // Make sure the message is valid 77 | if len(tx.Msgs) != 1 { 78 | t.Fatalf("invalid number of transaction messages, %d", len(tx.Msgs)) 79 | } 80 | 81 | assert.Equal(t, msg, tx.Msgs[0]) 82 | assert.NotEmpty(t, tx.Msgs[0].GetSigners()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/runtime/package_deployment.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gnolang/gno/gno.land/pkg/sdk/vm" 8 | "github.com/gnolang/gno/gnovm" 9 | "github.com/gnolang/gno/tm2/pkg/crypto" 10 | "github.com/gnolang/gno/tm2/pkg/std" 11 | ) 12 | 13 | type packageDeployment struct{} 14 | 15 | func newPackageDeployment() *packageDeployment { 16 | return &packageDeployment{} 17 | } 18 | 19 | func (c *packageDeployment) Initialize( 20 | _ std.Account, 21 | _ crypto.PrivKey, 22 | _ string, 23 | _ EstimateGasFn, 24 | ) ([]*std.Tx, error) { 25 | // No extra setup needed for this runtime type 26 | return nil, nil 27 | } 28 | 29 | func (c *packageDeployment) ConstructTransactions( 30 | keys []crypto.PrivKey, 31 | accounts []std.Account, 32 | transactions uint64, 33 | chainID string, 34 | estimateFn EstimateGasFn, 35 | ) ([]*std.Tx, error) { 36 | var ( 37 | timestamp = time.Now().Unix() 38 | 39 | getMsgFn = func(creator std.Account, index int) std.Msg { 40 | memPkg := &gnovm.MemPackage{ 41 | Name: packageName, 42 | Path: fmt.Sprintf( 43 | "%s/%s/stress_%d_%d", 44 | packagePathPrefix, 45 | creator.GetAddress().String(), 46 | timestamp, 47 | index, 48 | ), 49 | Files: []*gnovm.MemFile{ 50 | { 51 | Name: packageFileName, 52 | Body: packageBody, 53 | }, 54 | }, 55 | } 56 | 57 | return vm.MsgAddPackage{ 58 | Creator: creator.GetAddress(), 59 | Package: memPkg, 60 | } 61 | } 62 | ) 63 | 64 | return constructTransactions( 65 | keys, 66 | accounts, 67 | transactions, 68 | chainID, 69 | getMsgFn, 70 | estimateFn, 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /internal/runtime/realm_call.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gnolang/gno/gno.land/pkg/sdk/vm" 8 | "github.com/gnolang/gno/gnovm" 9 | "github.com/gnolang/gno/tm2/pkg/crypto" 10 | "github.com/gnolang/gno/tm2/pkg/std" 11 | "github.com/gnolang/supernova/internal/common" 12 | "github.com/gnolang/supernova/internal/signer" 13 | ) 14 | 15 | const methodName = "SayHello" 16 | 17 | type realmCall struct { 18 | realmPath string 19 | } 20 | 21 | func newRealmCall() *realmCall { 22 | return &realmCall{} 23 | } 24 | 25 | func (r *realmCall) Initialize( 26 | account std.Account, 27 | key crypto.PrivKey, 28 | chainID string, 29 | estimateFn EstimateGasFn, 30 | ) ([]*std.Tx, error) { 31 | // The Realm needs to be deployed before 32 | // it can be interacted with 33 | r.realmPath = fmt.Sprintf( 34 | "%s/%s/stress_%d", 35 | realmPathPrefix, 36 | account.GetAddress().String(), 37 | time.Now().Unix(), 38 | ) 39 | 40 | // Construct the transaction 41 | msg := vm.MsgAddPackage{ 42 | Creator: account.GetAddress(), 43 | Package: &gnovm.MemPackage{ 44 | Name: packageName, 45 | Path: r.realmPath, 46 | Files: []*gnovm.MemFile{ 47 | { 48 | Name: realmFileName, 49 | Body: realmBody, 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | tx := &std.Tx{ 56 | Msgs: []std.Msg{msg}, 57 | Fee: common.CalculateFeeInRatio(1_000_000, common.DefaultGasPrice), 58 | } 59 | 60 | // Sign it 61 | cfg := signer.SignCfg{ 62 | ChainID: chainID, 63 | AccountNumber: account.GetAccountNumber(), 64 | Sequence: account.GetSequence(), 65 | } 66 | 67 | if err := signer.SignTx(tx, key, cfg); err != nil { 68 | return nil, fmt.Errorf("unable to sign initialize transaction, %w", err) 69 | } 70 | 71 | // Estimate the gas for the initial tx 72 | gasWanted, err := estimateFn(tx) 73 | if err != nil { 74 | return nil, fmt.Errorf("unable to estimate gas: %w", err) 75 | } 76 | 77 | // Wipe the signatures, because we will change the fee, 78 | // and cause the previous ones to be invalid 79 | clear(tx.Signatures) 80 | 81 | tx.Fee = common.CalculateFeeInRatio(gasWanted+gasBuffer, common.DefaultGasPrice) // buffer with 10k gas 82 | 83 | if err = signer.SignTx(tx, key, cfg); err != nil { 84 | return nil, fmt.Errorf("unable to sign initialize transaction, %w", err) 85 | } 86 | 87 | return []*std.Tx{tx}, nil 88 | } 89 | 90 | func (r *realmCall) ConstructTransactions( 91 | keys []crypto.PrivKey, 92 | accounts []std.Account, 93 | transactions uint64, 94 | chainID string, 95 | estimateFn EstimateGasFn, 96 | ) ([]*std.Tx, error) { 97 | getMsgFn := func(creator std.Account, index int) std.Msg { 98 | return vm.MsgCall{ 99 | Caller: creator.GetAddress(), 100 | PkgPath: r.realmPath, 101 | Func: methodName, 102 | Args: []string{fmt.Sprintf("Account-%d", index)}, 103 | } 104 | } 105 | 106 | return constructTransactions( 107 | keys, 108 | accounts, 109 | transactions, 110 | chainID, 111 | getMsgFn, 112 | estimateFn, 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /internal/runtime/realm_deployment.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gnolang/gno/gno.land/pkg/sdk/vm" 8 | "github.com/gnolang/gno/gnovm" 9 | "github.com/gnolang/gno/tm2/pkg/crypto" 10 | "github.com/gnolang/gno/tm2/pkg/std" 11 | ) 12 | 13 | type realmDeployment struct{} 14 | 15 | func newRealmDeployment() *realmDeployment { 16 | return &realmDeployment{} 17 | } 18 | 19 | func (c *realmDeployment) Initialize( 20 | _ std.Account, 21 | _ crypto.PrivKey, 22 | _ string, 23 | _ EstimateGasFn, 24 | ) ([]*std.Tx, error) { 25 | // No extra setup needed for this runtime type 26 | return nil, nil 27 | } 28 | 29 | func (c *realmDeployment) ConstructTransactions( 30 | keys []crypto.PrivKey, 31 | accounts []std.Account, 32 | transactions uint64, 33 | chainID string, 34 | estimateFn EstimateGasFn, 35 | ) ([]*std.Tx, error) { 36 | var ( 37 | timestamp = time.Now().Unix() 38 | 39 | getMsgFn = func(creator std.Account, index int) std.Msg { 40 | memPkg := &gnovm.MemPackage{ 41 | Name: packageName, 42 | Path: fmt.Sprintf( 43 | "%s/%s/stress_%d_%d", 44 | realmPathPrefix, 45 | creator.GetAddress().String(), 46 | timestamp, 47 | index, 48 | ), 49 | Files: []*gnovm.MemFile{ 50 | { 51 | Name: realmFileName, 52 | Body: realmBody, 53 | }, 54 | }, 55 | } 56 | 57 | return vm.MsgAddPackage{ 58 | Creator: creator.GetAddress(), 59 | Package: memPkg, 60 | } 61 | } 62 | ) 63 | 64 | return constructTransactions( 65 | keys, 66 | accounts, 67 | transactions, 68 | chainID, 69 | getMsgFn, 70 | estimateFn, 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /internal/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "github.com/gnolang/gno/tm2/pkg/crypto" 5 | "github.com/gnolang/gno/tm2/pkg/std" 6 | ) 7 | 8 | const ( 9 | realmPathPrefix = "gno.land/r" 10 | packagePathPrefix = "gno.land/p" 11 | ) 12 | 13 | // EstimateGasFn is the gas estimation callback 14 | type EstimateGasFn func(tx *std.Tx) (int64, error) 15 | 16 | // Runtime is the base interface for all runtime 17 | // implementations. 18 | // 19 | // The runtime's job is to prepare the transactions for the stress test (generate + sign), 20 | // and to predeploy (initialize) any infrastructure (package) 21 | type Runtime interface { 22 | // Initialize prepares any infrastructure transactions that are required 23 | // to be executed before the stress test runs, if any 24 | Initialize( 25 | account std.Account, 26 | key crypto.PrivKey, 27 | chainID string, 28 | estimateFn EstimateGasFn, 29 | ) ([]*std.Tx, error) 30 | 31 | // ConstructTransactions generates and signs the required transactions 32 | // that will be used in the stress test 33 | ConstructTransactions( 34 | keys []crypto.PrivKey, 35 | accounts []std.Account, 36 | transactions uint64, 37 | chainID string, 38 | estimateFn EstimateGasFn, 39 | ) ([]*std.Tx, error) 40 | } 41 | 42 | // GetRuntime fetches the specified runtime, if any 43 | func GetRuntime(runtimeType Type) Runtime { 44 | switch runtimeType { 45 | case RealmCall: 46 | return newRealmCall() 47 | case RealmDeployment: 48 | return newRealmDeployment() 49 | case PackageDeployment: 50 | return newPackageDeployment() 51 | default: 52 | return nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/runtime/runtime_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gnolang/gno/gno.land/pkg/sdk/vm" 7 | "github.com/gnolang/gno/tm2/pkg/std" 8 | "github.com/gnolang/supernova/internal/common" 9 | testutils "github.com/gnolang/supernova/internal/testing" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // verifyDeployTxCommon does common transaction verification 14 | func verifyDeployTxCommon(t *testing.T, tx *std.Tx, expectedPrefix string) { 15 | t.Helper() 16 | 17 | if len(tx.Msgs) != 1 { 18 | t.Fatalf("invalid number of tx messages, %d", len(tx.Msgs)) 19 | } 20 | 21 | msg := tx.Msgs[0] 22 | 23 | vmMsg, ok := msg.(vm.MsgAddPackage) 24 | if !ok { 25 | t.Fatal("invalid tx message type") 26 | } 27 | 28 | // Make sure the deploy params are valid 29 | assert.Contains(t, vmMsg.Package.Path, expectedPrefix) 30 | assert.Len(t, vmMsg.Package.Files, 1) 31 | assert.NotNil(t, vmMsg.Creator) 32 | assert.Nil(t, vmMsg.Deposit) 33 | 34 | // Make sure the fee is valid 35 | assert.Equal( 36 | t, 37 | common.CalculateFeeInRatio(1_000_000+gasBuffer, common.DefaultGasPrice), 38 | tx.Fee, 39 | ) 40 | } 41 | 42 | func TestRuntime_CommonDeployment(t *testing.T) { 43 | t.Parallel() 44 | 45 | testTable := []struct { 46 | name string 47 | mode Type 48 | expectedPrefix string 49 | }{ 50 | { 51 | "Realm Deployment", 52 | RealmDeployment, 53 | realmPathPrefix, 54 | }, 55 | { 56 | "Package Deployment", 57 | PackageDeployment, 58 | packagePathPrefix, 59 | }, 60 | } 61 | 62 | for _, testCase := range testTable { 63 | t.Run(testCase.name, func(t *testing.T) { 64 | t.Parallel() 65 | 66 | var ( 67 | transactions = uint64(100) 68 | accounts = generateAccounts(10) 69 | accountKeys = testutils.GenerateAccounts(t, 10) 70 | ) 71 | 72 | // Get the runtime 73 | r := GetRuntime(testCase.mode) 74 | 75 | // Make sure there is no initialization logic 76 | initialTxs, err := r.Initialize( 77 | accounts[0], 78 | accountKeys[0], 79 | "dummy", 80 | func(_ *std.Tx) (int64, error) { 81 | return 1_000_000, nil 82 | }, 83 | ) 84 | 85 | assert.Nil(t, initialTxs) 86 | assert.Nil(t, err) 87 | 88 | // Construct the transactions 89 | txs, err := r.ConstructTransactions( 90 | accountKeys, 91 | accounts, 92 | transactions, 93 | "dummy", 94 | func(_ *std.Tx) (int64, error) { 95 | return 1_000_000, nil 96 | }, 97 | ) 98 | if err != nil { 99 | t.Fatalf("unable to construct transactions, %v", err) 100 | } 101 | 102 | // Make sure they were constructed properly 103 | if len(txs) != int(transactions) { 104 | t.Fatalf("invalid number of transactions constructed, %d", len(txs)) 105 | } 106 | 107 | for _, tx := range txs { 108 | verifyDeployTxCommon(t, tx, testCase.expectedPrefix) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | func TestRuntime_RealmCall(t *testing.T) { 115 | t.Parallel() 116 | 117 | var ( 118 | transactions = uint64(100) 119 | accounts = generateAccounts(11) 120 | accountKeys = testutils.GenerateAccounts(t, 11) 121 | ) 122 | 123 | // Get the runtime 124 | r := GetRuntime(RealmCall) 125 | 126 | // Make sure the initialization logic is present 127 | initialTxs, err := r.Initialize( 128 | accounts[0], 129 | accountKeys[0], 130 | "dummy", 131 | func(_ *std.Tx) (int64, error) { 132 | return 1_000_000, nil 133 | }, 134 | ) 135 | if err != nil { 136 | t.Fatalf("unable to generate init transactions, %v", err) 137 | } 138 | 139 | if len(initialTxs) != 1 { 140 | t.Fatalf("invalid number of initial transactions, %d", len(initialTxs)) 141 | } 142 | 143 | for _, tx := range initialTxs { 144 | verifyDeployTxCommon(t, tx, realmPathPrefix) 145 | } 146 | 147 | // Construct the transactions 148 | txs, err := r.ConstructTransactions( 149 | accountKeys[1:], 150 | accounts[1:], 151 | transactions, 152 | "dummy", 153 | func(_ *std.Tx) (int64, error) { 154 | return 1_000_000, nil 155 | }, 156 | ) 157 | if err != nil { 158 | t.Fatalf("unable to construct transactions, %v", err) 159 | } 160 | 161 | // Make sure they were constructed properly 162 | if len(txs) != int(transactions) { 163 | t.Fatalf("invalid number of transactions constructed, %d", len(txs)) 164 | } 165 | 166 | for _, tx := range txs { 167 | if len(tx.Msgs) != 1 { 168 | t.Fatalf("invalid number of tx messages, %d", len(tx.Msgs)) 169 | } 170 | 171 | msg := tx.Msgs[0] 172 | 173 | vmMsg, ok := msg.(vm.MsgCall) 174 | if !ok { 175 | t.Fatal("invalid tx message type") 176 | } 177 | 178 | // Make sure the call params are valid 179 | assert.Equal(t, vmMsg.Func, methodName) 180 | assert.NotNil(t, vmMsg.Caller) 181 | assert.Nil(t, vmMsg.Send) 182 | 183 | if len(vmMsg.Args) != 1 { 184 | t.Fatalf("invalid number of arguments provided for call") 185 | } 186 | 187 | assert.Contains(t, vmMsg.Args[0], "Account") 188 | 189 | // Make sure the fee is valid 190 | assert.Equal( 191 | t, 192 | common.CalculateFeeInRatio(1_000_000+gasBuffer, common.DefaultGasPrice), 193 | tx.Fee, 194 | ) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /internal/runtime/source.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | const ( 4 | realmBody = `package runtime 5 | 6 | var greeting string 7 | 8 | func init() { 9 | greeting = "Hello" 10 | } 11 | 12 | // SayHello says hello to the specified name, using 13 | // the saved greeting 14 | func SayHello(name string) string { 15 | return greeting + " " + name + "!" 16 | } 17 | ` 18 | packageBody = `package runtime 19 | 20 | type Language string 21 | 22 | const ( 23 | French Language = "french" 24 | Italian Language = "italian" 25 | Spanish Language = "spanish" 26 | Hindi Language = "hindi" 27 | Bulgarian Language = "bulgarian" 28 | Serbian Language = "serbian" 29 | ) 30 | 31 | // GetGreeting generates a greeting in 32 | // the specified language 33 | func GetGreeting(language Language) string { 34 | switch language { 35 | case French: 36 | return "Bonjour" 37 | case Italian: 38 | return "Ciao" 39 | case Spanish: 40 | return "Hola" 41 | case Hindi: 42 | return "नमस्ते" 43 | case Bulgarian: 44 | return "Здравейте" 45 | case Serbian: 46 | return "Здраво" 47 | default: 48 | return "Hello" 49 | } 50 | } 51 | ` 52 | ) 53 | 54 | const ( 55 | packageName = "runtime" 56 | realmFileName = "realm.gno" 57 | packageFileName = "package.gno" 58 | ) 59 | -------------------------------------------------------------------------------- /internal/runtime/type.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | type Type string 4 | 5 | const ( 6 | RealmDeployment Type = "REALM_DEPLOYMENT" 7 | PackageDeployment Type = "PACKAGE_DEPLOYMENT" 8 | RealmCall Type = "REALM_CALL" 9 | unknown Type = "UNKNOWN" 10 | ) 11 | 12 | // IsRuntime checks if the passed in runtime 13 | // is a supported runtime type 14 | func IsRuntime(runtime Type) bool { 15 | return runtime == RealmCall || 16 | runtime == RealmDeployment || 17 | runtime == PackageDeployment 18 | } 19 | 20 | // String returns a string representation 21 | // of the runtime type 22 | func (r Type) String() string { 23 | switch r { 24 | case RealmDeployment: 25 | return string(RealmDeployment) 26 | case PackageDeployment: 27 | return string(PackageDeployment) 28 | case RealmCall: 29 | return string(RealmCall) 30 | default: 31 | return string(unknown) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/runtime/type_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestType_IsRuntime(t *testing.T) { 10 | t.Parallel() 11 | 12 | testTable := []struct { 13 | name string 14 | mode Type 15 | isValid bool 16 | }{ 17 | { 18 | "Realm Deployment", 19 | RealmDeployment, 20 | true, 21 | }, 22 | { 23 | "Package Deployment", 24 | PackageDeployment, 25 | true, 26 | }, 27 | { 28 | "Realm Call", 29 | RealmCall, 30 | true, 31 | }, 32 | { 33 | "Dummy mode", 34 | Type("Dummy mode"), 35 | false, 36 | }, 37 | } 38 | 39 | for _, testCase := range testTable { 40 | t.Run(testCase.name, func(t *testing.T) { 41 | t.Parallel() 42 | 43 | assert.Equal(t, testCase.isValid, IsRuntime(testCase.mode)) 44 | }) 45 | } 46 | } 47 | 48 | func TestType_String(t *testing.T) { 49 | t.Parallel() 50 | 51 | testTable := []struct { 52 | name string 53 | mode Type 54 | expectedStr string 55 | }{ 56 | { 57 | "Realm Deployment", 58 | RealmDeployment, 59 | string(RealmDeployment), 60 | }, 61 | { 62 | "Package Deployment", 63 | PackageDeployment, 64 | string(PackageDeployment), 65 | }, 66 | { 67 | "Realm Call", 68 | RealmCall, 69 | string(RealmCall), 70 | }, 71 | { 72 | "Dummy mode", 73 | Type("Dummy mode"), 74 | string(unknown), 75 | }, 76 | } 77 | 78 | for _, testCase := range testTable { 79 | t.Run(testCase.name, func(t *testing.T) { 80 | t.Parallel() 81 | 82 | assert.Equal(t, testCase.expectedStr, testCase.mode.String()) 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /internal/signer/signer.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gnolang/gno/tm2/pkg/crypto" 7 | "github.com/gnolang/gno/tm2/pkg/crypto/hd" 8 | "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" 9 | "github.com/gnolang/gno/tm2/pkg/std" 10 | ) 11 | 12 | // SignCfg specifies the sign configuration 13 | type SignCfg struct { 14 | ChainID string // the ID of the chain 15 | AccountNumber uint64 // the account number of the signer 16 | Sequence uint64 // the Sequence of the signer 17 | } 18 | 19 | // SignTx signs the specified transaction using 20 | // the provided key and config 21 | func SignTx(tx *std.Tx, key crypto.PrivKey, cfg SignCfg) error { 22 | // Get the sign bytes 23 | signBytes, err := tx.GetSignBytes( 24 | cfg.ChainID, 25 | cfg.AccountNumber, 26 | cfg.Sequence, 27 | ) 28 | if err != nil { 29 | return fmt.Errorf("unable to get tx signature payload, %w", err) 30 | } 31 | 32 | // Sign the transaction 33 | signature, err := key.Sign(signBytes) 34 | if err != nil { 35 | return fmt.Errorf("unable to sign transaction, %w", err) 36 | } 37 | 38 | // Save the signature 39 | tx.Signatures = append(tx.Signatures, std.Signature{ 40 | PubKey: key.PubKey(), 41 | Signature: signature, 42 | }) 43 | 44 | return nil 45 | } 46 | 47 | // GenerateKeyFromSeed generates a private key from 48 | // the provided seed and index 49 | func GenerateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { 50 | pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) 51 | 52 | masterPriv, ch := hd.ComputeMastersFromSeed(seed) 53 | 54 | //nolint:errcheck // This derivation can never error out, since the path params 55 | // are always going to be valid 56 | derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) 57 | 58 | return secp256k1.PrivKeySecp256k1(derivedPriv) 59 | } 60 | -------------------------------------------------------------------------------- /internal/testing/testing.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gnolang/gno/tm2/pkg/crypto" 7 | "github.com/gnolang/gno/tm2/pkg/crypto/bip39" 8 | "github.com/gnolang/supernova/internal/signer" 9 | ) 10 | 11 | // GenerateMnemonic generates a new BIP39 mnemonic 12 | func GenerateMnemonic(t *testing.T) string { 13 | t.Helper() 14 | 15 | // Generate the entropy seed 16 | entropySeed, err := bip39.NewEntropy(256) 17 | if err != nil { 18 | t.Fatalf("unable to generate entropy seed, %v", err) 19 | } 20 | 21 | // Generate the actual mnemonic 22 | mnemonic, err := bip39.NewMnemonic(entropySeed) 23 | if err != nil { 24 | t.Fatalf("unable to generate mnemonic, %v", err) 25 | } 26 | 27 | return mnemonic 28 | } 29 | 30 | // GenerateAccounts generates mock keybase accounts 31 | func GenerateAccounts(t *testing.T, count int) []crypto.PrivKey { 32 | t.Helper() 33 | 34 | var ( 35 | accounts = make([]crypto.PrivKey, count) 36 | mnemonic = GenerateMnemonic(t) 37 | seed = bip39.NewSeed(mnemonic, "") 38 | ) 39 | 40 | for i := 0; i < count; i++ { 41 | accounts[i] = signer.GenerateKeyFromSeed(seed, uint32(i)) 42 | } 43 | 44 | return accounts 45 | } 46 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gnolang/gno/supernova/tools 2 | 3 | go 1.23 4 | 5 | require github.com/golangci/golangci-lint v1.63.4 6 | 7 | require ( 8 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 9 | 4d63.com/gochecknoglobals v0.2.1 // indirect 10 | github.com/4meepo/tagalign v1.4.1 // indirect 11 | github.com/Abirdcfly/dupword v0.1.3 // indirect 12 | github.com/Antonboom/errname v1.0.0 // indirect 13 | github.com/Antonboom/nilnil v1.0.1 // indirect 14 | github.com/Antonboom/testifylint v1.5.2 // indirect 15 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 16 | github.com/Crocmagnon/fatcontext v0.5.3 // indirect 17 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 18 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect 19 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 20 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 21 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 22 | github.com/alexkohler/nakedret/v2 v2.0.5 // indirect 23 | github.com/alexkohler/prealloc v1.0.0 // indirect 24 | github.com/alingse/asasalint v0.0.11 // indirect 25 | github.com/alingse/nilnesserr v0.1.1 // indirect 26 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 27 | github.com/ashanbrown/makezero v1.2.0 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/bkielbasa/cyclop v1.2.3 // indirect 30 | github.com/blizzy78/varnamelen v0.8.0 // indirect 31 | github.com/bombsimon/wsl/v4 v4.5.0 // indirect 32 | github.com/breml/bidichk v0.3.2 // indirect 33 | github.com/breml/errchkjson v0.4.0 // indirect 34 | github.com/butuzov/ireturn v0.3.1 // indirect 35 | github.com/butuzov/mirror v1.3.0 // indirect 36 | github.com/catenacyber/perfsprint v0.7.1 // indirect 37 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 38 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 39 | github.com/charithe/durationcheck v0.0.10 // indirect 40 | github.com/chavacava/garif v0.1.0 // indirect 41 | github.com/ckaznocha/intrange v0.3.0 // indirect 42 | github.com/curioswitch/go-reassign v0.3.0 // indirect 43 | github.com/daixiang0/gci v0.13.5 // indirect 44 | github.com/davecgh/go-spew v1.1.1 // indirect 45 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 46 | github.com/ettle/strcase v0.2.0 // indirect 47 | github.com/fatih/color v1.18.0 // indirect 48 | github.com/fatih/structtag v1.2.0 // indirect 49 | github.com/firefart/nonamedreturns v1.0.5 // indirect 50 | github.com/fsnotify/fsnotify v1.5.4 // indirect 51 | github.com/fzipp/gocyclo v0.6.0 // indirect 52 | github.com/ghostiam/protogetter v0.3.8 // indirect 53 | github.com/go-critic/go-critic v0.11.5 // indirect 54 | github.com/go-toolsmith/astcast v1.1.0 // indirect 55 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 56 | github.com/go-toolsmith/astequal v1.2.0 // indirect 57 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 58 | github.com/go-toolsmith/astp v1.1.0 // indirect 59 | github.com/go-toolsmith/strparse v1.1.0 // indirect 60 | github.com/go-toolsmith/typep v1.1.0 // indirect 61 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 62 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 63 | github.com/gobwas/glob v0.2.3 // indirect 64 | github.com/gofrs/flock v0.12.1 // indirect 65 | github.com/golang/protobuf v1.5.3 // indirect 66 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 67 | github.com/golangci/go-printf-func-name v0.1.0 // indirect 68 | github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9 // indirect 69 | github.com/golangci/misspell v0.6.0 // indirect 70 | github.com/golangci/plugin-module-register v0.1.1 // indirect 71 | github.com/golangci/revgrep v0.5.3 // indirect 72 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 73 | github.com/google/go-cmp v0.6.0 // indirect 74 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 75 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 76 | github.com/gostaticanalysis/comment v1.4.2 // indirect 77 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 78 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 79 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 80 | github.com/hashicorp/go-version v1.7.0 // indirect 81 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 82 | github.com/hashicorp/hcl v1.0.0 // indirect 83 | github.com/hexops/gotextdiff v1.0.3 // indirect 84 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 85 | github.com/jgautheron/goconst v1.7.1 // indirect 86 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 87 | github.com/jjti/go-spancheck v0.6.4 // indirect 88 | github.com/julz/importas v0.2.0 // indirect 89 | github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect 90 | github.com/kisielk/errcheck v1.8.0 // indirect 91 | github.com/kkHAIKE/contextcheck v1.1.5 // indirect 92 | github.com/kulti/thelper v0.6.3 // indirect 93 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 94 | github.com/kyoh86/exportloopref v0.1.11 // indirect 95 | github.com/lasiar/canonicalheader v1.1.2 // indirect 96 | github.com/ldez/exptostd v0.3.1 // indirect 97 | github.com/ldez/gomoddirectives v0.6.0 // indirect 98 | github.com/ldez/grignotin v0.7.0 // indirect 99 | github.com/ldez/tagliatelle v0.7.1 // indirect 100 | github.com/ldez/usetesting v0.4.2 // indirect 101 | github.com/leonklingele/grouper v1.1.2 // indirect 102 | github.com/macabu/inamedparam v0.1.3 // indirect 103 | github.com/magiconair/properties v1.8.6 // indirect 104 | github.com/maratori/testableexamples v1.0.0 // indirect 105 | github.com/maratori/testpackage v1.1.1 // indirect 106 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 107 | github.com/mattn/go-colorable v0.1.13 // indirect 108 | github.com/mattn/go-isatty v0.0.20 // indirect 109 | github.com/mattn/go-runewidth v0.0.16 // indirect 110 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 111 | github.com/mgechev/revive v1.5.1 // indirect 112 | github.com/mitchellh/go-homedir v1.1.0 // indirect 113 | github.com/mitchellh/mapstructure v1.5.0 // indirect 114 | github.com/moricho/tparallel v0.3.2 // indirect 115 | github.com/nakabonne/nestif v0.3.1 // indirect 116 | github.com/nishanths/exhaustive v0.12.0 // indirect 117 | github.com/nishanths/predeclared v0.2.2 // indirect 118 | github.com/nunnatsa/ginkgolinter v0.18.4 // indirect 119 | github.com/olekukonko/tablewriter v0.0.5 // indirect 120 | github.com/pelletier/go-toml v1.9.5 // indirect 121 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 122 | github.com/pmezard/go-difflib v1.0.0 // indirect 123 | github.com/polyfloyd/go-errorlint v1.7.0 // indirect 124 | github.com/prometheus/client_golang v1.12.1 // indirect 125 | github.com/prometheus/client_model v0.2.0 // indirect 126 | github.com/prometheus/common v0.32.1 // indirect 127 | github.com/prometheus/procfs v0.7.3 // indirect 128 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 129 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 130 | github.com/quasilyte/gogrep v0.5.0 // indirect 131 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 132 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 133 | github.com/raeperd/recvcheck v0.2.0 // indirect 134 | github.com/rivo/uniseg v0.4.7 // indirect 135 | github.com/rogpeppe/go-internal v1.13.1 // indirect 136 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 137 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 138 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 139 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect 140 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 141 | github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect 142 | github.com/securego/gosec/v2 v2.21.4 // indirect 143 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 144 | github.com/sirupsen/logrus v1.9.3 // indirect 145 | github.com/sivchari/containedctx v1.0.3 // indirect 146 | github.com/sivchari/tenv v1.12.1 // indirect 147 | github.com/sonatard/noctx v0.1.0 // indirect 148 | github.com/sourcegraph/go-diff v0.7.0 // indirect 149 | github.com/spf13/afero v1.11.0 // indirect 150 | github.com/spf13/cast v1.5.0 // indirect 151 | github.com/spf13/cobra v1.8.1 // indirect 152 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 153 | github.com/spf13/pflag v1.0.5 // indirect 154 | github.com/spf13/viper v1.12.0 // indirect 155 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 156 | github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect 157 | github.com/stretchr/objx v0.5.2 // indirect 158 | github.com/stretchr/testify v1.10.0 // indirect 159 | github.com/subosito/gotenv v1.4.1 // indirect 160 | github.com/tdakkota/asciicheck v0.3.0 // indirect 161 | github.com/tetafro/godot v1.4.20 // indirect 162 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect 163 | github.com/timonwong/loggercheck v0.10.1 // indirect 164 | github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect 165 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 166 | github.com/ultraware/funlen v0.2.0 // indirect 167 | github.com/ultraware/whitespace v0.2.0 // indirect 168 | github.com/uudashr/gocognit v1.2.0 // indirect 169 | github.com/uudashr/iface v1.3.0 // indirect 170 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 171 | github.com/yagipy/maintidx v1.0.0 // indirect 172 | github.com/yeya24/promlinter v0.3.0 // indirect 173 | github.com/ykadowak/zerologlint v0.1.5 // indirect 174 | gitlab.com/bosi/decorder v0.4.2 // indirect 175 | go-simpler.org/musttag v0.13.0 // indirect 176 | go-simpler.org/sloglint v0.7.2 // indirect 177 | go.uber.org/atomic v1.7.0 // indirect 178 | go.uber.org/automaxprocs v1.6.0 // indirect 179 | go.uber.org/multierr v1.6.0 // indirect 180 | go.uber.org/zap v1.24.0 // indirect 181 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 182 | golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect 183 | golang.org/x/mod v0.22.0 // indirect 184 | golang.org/x/sync v0.10.0 // indirect 185 | golang.org/x/sys v0.28.0 // indirect 186 | golang.org/x/text v0.20.0 // indirect 187 | golang.org/x/tools v0.28.0 // indirect 188 | google.golang.org/protobuf v1.34.2 // indirect 189 | gopkg.in/ini.v1 v1.67.0 // indirect 190 | gopkg.in/yaml.v2 v2.4.0 // indirect 191 | gopkg.in/yaml.v3 v3.0.1 // indirect 192 | honnef.co/go/tools v0.5.1 // indirect 193 | mvdan.cc/gofumpt v0.7.0 // indirect 194 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 195 | ) 196 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 5 | ) 6 | --------------------------------------------------------------------------------