diff --git a/.github/golangci.yml b/.github/golangci.yml index 8b38691ec75..e78d09a582e 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -38,6 +38,7 @@ linters: - gofumpt # Stricter gofmt - unused # Checks Go code for unused constants, variables, functions and types - gomodguard # Enforces an allow and block list for direct Go module dependencies + - forbidigo # Forbids some custom-set identifiers, like regexp.MatchString linters-settings: gofmt: @@ -60,6 +61,10 @@ linters-settings: - opinionated - performance - style + forbidigo: + forbid: + - p: '^regexp\.(Match|MatchString)$' + msg: it will re-compile the regexp for each execution; compile the regexp with regexp.Compile and store it as a singleton issues: whole-files: true diff --git a/.github/goreleaser-nightly.yaml b/.github/goreleaser-nightly.yaml new file mode 100644 index 00000000000..f879b37657d --- /dev/null +++ b/.github/goreleaser-nightly.yaml @@ -0,0 +1,485 @@ +project_name: gno + +before: + hooks: + - go mod tidy + +builds: + - id: gno + main: ./gnovm/cmd/gno + binary: gno + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnoland + main: ./gno.land/cmd/gnoland + binary: gnoland + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnokey + main: ./gno.land/cmd/gnokey + binary: gnokey + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnoweb + main: ./gno.land/cmd/gnoweb + binary: gnoweb + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 +gomod: + proxy: true + +archives: + # https://goreleaser.com/customization/archive/ + - files: + # Standard Release Files + - LICENSE.md + - README.md + +signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + certificate: '${artifact}.pem' + args: + - sign-blob + - '--output-certificate=${certificate}' + - '--output-signature=${signature}' + - '${artifact}' + - "--yes" # needed on cosign 2.0.0+ + artifacts: checksum + output: true + +dockers: + # https://goreleaser.com/customization/docker/ + + # gno + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + + # gnoland + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + # gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + + # gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + +docker_manifests: + # https://goreleaser.com/customization/docker_manifest/ + + # gno + - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}:nightly + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}:nightly-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}:nightly-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv7 + + # gnoland + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv7 + + # gnokey + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv7 + + # gnoweb + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv7 + +docker_signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + artifacts: images + output: true + args: + - 'sign' + - '${artifact}' + - "--yes" # needed on cosign 2.0.0+ + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + +source: + enabled: true + +sboms: + - artifacts: archive + - id: source # Two different sbom configurations need two different IDs + artifacts: source + +release: + draft: true + replace_existing_draft: true + prerelease: auto + mode: append + footer: | + ### Container Images + + You can find all docker images at: + + https://github.com/orgs/gnolang/packages?repo_name={{ .ProjectName }} + +nightly: + tag_name: nightly + publish_release: true + keep_single_release: true \ No newline at end of file diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml new file mode 100644 index 00000000000..d82b31ea9fb --- /dev/null +++ b/.github/goreleaser.yaml @@ -0,0 +1,485 @@ +project_name: gno + +before: + hooks: + - go mod tidy + +builds: + - id: gno + main: ./gnovm/cmd/gno + binary: gno + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnoland + main: ./gno.land/cmd/gnoland + binary: gnoland + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnokey + main: ./gno.land/cmd/gnokey + binary: gnokey + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + - id: gnoweb + main: ./gno.land/cmd/gnoweb + binary: gnoweb + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 +gomod: + proxy: true + +archives: + # https://goreleaser.com/customization/archive/ + - files: + # Standard Release Files + - LICENSE.md + - README.md + +signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + certificate: '${artifact}.pem' + args: + - sign-blob + - '--output-certificate=${certificate}' + - '--output-signature=${signature}' + - '${artifact}' + - "--yes" # needed on cosign 2.0.0+ + artifacts: checksum + output: true + +dockers: + # https://goreleaser.com/customization/docker/ + + # gno + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}:latest-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}:latest-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + - use: buildx + dockerfile: Dockerfile.gno.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}:latest-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gno + extra_files: + - examples + - gnovm/stdlibs + - gnovm/tests/stdlibs + + # gnoland + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.gnoland.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoland + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + # gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + - use: buildx + dockerfile: Dockerfile.gnokey.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnokey + + # gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-arm64v8" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-armv6" + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + - use: buildx + dockerfile: Dockerfile.gnoweb.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-armv7" + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnoweb + +docker_manifests: + # https://goreleaser.com/customization/docker_manifest/ + + # gno + - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}:latest + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}:latest-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}:latest-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}:latest-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}:latest-armv7 + + # gnoland + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:latest-armv7 + + # gnokey + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:latest-armv7 + + # gnoweb + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:latest-armv7 + +docker_signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + artifacts: images + output: true + args: + - 'sign' + - '${artifact}' + - "--yes" # needed on cosign 2.0.0+ + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + +source: + enabled: true + +sboms: + - artifacts: archive + - id: source # Two different sbom configurations need two different IDs + artifacts: source + +release: + draft: true + replace_existing_draft: true + prerelease: auto + mode: append + footer: | + ### Container Images + + You can find all docker images at: + + https://github.com/orgs/gnolang/packages?repo_name={{ .ProjectName }} + +nightly: + tag_name: nightly + publish_release: true + keep_single_release: true \ No newline at end of file diff --git a/.github/workflows/DELETEdocker.yml b/.github/workflows/DELETEdocker.yml deleted file mode 100644 index 374eab763c9..00000000000 --- a/.github/workflows/DELETEdocker.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: docker -on: - push: - branches: [ "master" ] - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - build-main: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Get commit SHA - id: commit - run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - if: (github.event_name != 'pull_request') - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: | - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.commit.outputs.sha }} - - build-slim: - runs-on: ubuntu-latest - strategy: - matrix: - target: [ gnoland-slim, gnokey-slim, gno-slim, gnofaucet-slim, gnoweb-slim ] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Get commit SHA - id: commit - run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - if: (github.event_name != 'pull_request') - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: . - platforms: linux/amd64,linux/arm64 - target: ${{ matrix.target }} - push: ${{ github.event_name != 'pull_request' }} - tags: | - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/${{ matrix.target }}:latest - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}/${{ matrix.target }}:${{ steps.commit.outputs.sha }} diff --git a/.github/workflows/autocounterd.yml b/.github/workflows/autocounterd.yml index 45149af82fa..c8251a05490 100644 --- a/.github/workflows/autocounterd.yml +++ b/.github/workflows/autocounterd.yml @@ -29,11 +29,11 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker metadata autcounterd + - name: Docker metadata autocounterd id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/${{ github.repository }}/autcounterd + images: ghcr.io/${{ github.repository }}/autocounterd tags: | type=raw,value=latest type=semver,pattern=v{{version}} diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml new file mode 100644 index 00000000000..a7a74f5bfa6 --- /dev/null +++ b/.github/workflows/nightlies.yml @@ -0,0 +1,45 @@ +name: Trigger nightly build + +on: + schedule: + - cron: '0 0 * * 2-6' + workflow_dispatch: + +permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: "1.22.x" + cache: true + + - uses: sigstore/cosign-installer@v3.5.0 + - uses: anchore/sbom-action/download-syft@v0.15.10 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --nightly --config ./.github/goreleaser-nightly.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml new file mode 100644 index 00000000000..bfb8f57c77b --- /dev/null +++ b/.github/workflows/releaser.yml @@ -0,0 +1,45 @@ +name: Go Releaser + +on: + push: + tags: + - "v*" + +permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: "1.22.x" + cache: true + + - uses: sigstore/cosign-installer@v3.5.0 + - uses: anchore/sbom-action/download-syft@v0.15.10 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --config ./.github/goreleaser.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.gitignore b/.gitignore index 0a06c2cf055..019c0be3c98 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ genesis.json # Build leftovers build +dist # Legacy .gitignore data/* diff --git a/.gitpod.yml b/.gitpod.yml index 794108f4613..49dec8ed15d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -37,7 +37,7 @@ ports: - name: "gnoland RPC" description: "the RPC server, managed by tendermint2" - port: 36657 + port: 26657 onOpen: notify github: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a18c7125a82..00000000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# build gno -FROM golang:1.22 AS build-gno -RUN mkdir -p /opt/gno/src /opt/build -WORKDIR /opt/build -ADD go.mod go.sum . -RUN go mod download -ADD . ./ -RUN go build -o ./build/gnoland ./gno.land/cmd/gnoland -RUN go build -o ./build/gnokey ./gno.land/cmd/gnokey -RUN go build -o ./build/gnoweb ./gno.land/cmd/gnoweb -RUN go build -o ./build/gno ./gnovm/cmd/gno -RUN ls -la ./build -ADD . /opt/gno/src/ -RUN rm -rf /opt/gno/src/.git - -# build faucet -FROM golang:1.22 AS build-faucet -RUN mkdir -p /opt/gno/src /opt/build -WORKDIR /opt/build -ADD contribs/gnofaucet/go.mod contribs/gnofaucet/go.sum . -RUN go mod download -ADD contribs/gnofaucet ./ -RUN go build -o ./build/gnofaucet . - - -# runtime-base + runtime-tls -FROM debian:stable-slim AS runtime-base -ENV PATH="${PATH}:/opt/gno/bin" \ - GNOROOT="/opt/gno/src" -WORKDIR /opt/gno/src -FROM runtime-base AS runtime-tls -RUN apt-get update && apt-get install -y expect ca-certificates && update-ca-certificates - -# slim images -FROM runtime-base AS gnoland-slim -WORKDIR /opt/gno/src/gno.land/ -COPY --from=build-gno /opt/build/build/gnoland /opt/gno/bin/ -ENTRYPOINT ["gnoland"] -EXPOSE 26657 36657 - -FROM runtime-base AS gnokey-slim -COPY --from=build-gno /opt/build/build/gnokey /opt/gno/bin/ -ENTRYPOINT ["gnokey"] - -FROM runtime-base AS gno-slim -COPY --from=build-gno /opt/build/build/gno /opt/gno/bin/ -ENTRYPOINT ["gno"] - -FROM runtime-tls AS gnofaucet-slim -COPY --from=build-faucet /opt/build/build/gnofaucet /opt/gno/bin/ -ENTRYPOINT ["gnofaucet"] -EXPOSE 5050 - -FROM runtime-tls AS gnoweb-slim -COPY --from=build-gno /opt/build/build/gnoweb /opt/gno/bin/ -COPY --from=build-gno /opt/gno/src/gno.land/cmd/gnoweb /opt/gno/src/gnoweb -ENTRYPOINT ["gnoweb"] -EXPOSE 8888 - -# all, contains everything. -FROM runtime-tls AS all -COPY --from=build-gno /opt/build/build/* /opt/gno/bin/ -COPY --from=build-faucet /opt/build/build/* /opt/gno/bin/ -COPY --from=build-gno /opt/gno/src /opt/gno/src -# gofmt is required by `gnokey maketx addpkg` -COPY --from=build-gno /usr/local/go/bin/gofmt /usr/bin diff --git a/Dockerfile.gno.release b/Dockerfile.gno.release new file mode 100644 index 00000000000..874d427661f --- /dev/null +++ b/Dockerfile.gno.release @@ -0,0 +1,11 @@ +FROM busybox + +COPY ./gno /gno +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ + +ENV GNOROOT="/gnoroot/" + +ENTRYPOINT [ "/gno" ] +CMD [ "" ] \ No newline at end of file diff --git a/Dockerfile.gnokey.release b/Dockerfile.gnokey.release new file mode 100644 index 00000000000..4261f9272d6 --- /dev/null +++ b/Dockerfile.gnokey.release @@ -0,0 +1,6 @@ +FROM busybox + +COPY . / + +ENTRYPOINT [ "/gnokey" ] +CMD [ "" ] \ No newline at end of file diff --git a/Dockerfile.gnoland.release b/Dockerfile.gnoland.release new file mode 100644 index 00000000000..302ad3fde8e --- /dev/null +++ b/Dockerfile.gnoland.release @@ -0,0 +1,13 @@ +FROM busybox + +COPY ./gnoland /gnoland +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl + +ENV GNOROOT="/gnoroot/" + +EXPOSE 26656 26657 +ENTRYPOINT [ "/gnoland" ] +CMD [ "" ] diff --git a/Dockerfile.gnoweb.release b/Dockerfile.gnoweb.release new file mode 100644 index 00000000000..9bdfd8ddce2 --- /dev/null +++ b/Dockerfile.gnoweb.release @@ -0,0 +1,7 @@ +FROM busybox + +COPY . / + +EXPOSE 8888 +ENTRYPOINT [ "/gnoweb" ] +CMD [ "" ] \ No newline at end of file diff --git a/Makefile b/Makefile index c2f5156e054..2dcff0d57a8 100644 --- a/Makefile +++ b/Makefile @@ -36,15 +36,15 @@ install: install.gnokey install.gno install.gnodev install.gnokey: $(MAKE) --no-print-directory -C ./gno.land install.gnokey # \033[0;32m ... \033[0m is ansi for green text. - @echo "\033[0;32m[+] 'gnokey' has been installed. Read more in ./gno.land/\033[0m" + @printf "\033[0;32m[+] 'gnokey' has been installed. Read more in ./gno.land/\033[0m\n" .PHONY: install.gno install.gno: $(MAKE) --no-print-directory -C ./gnovm install - @echo "\033[0;32m[+] 'gno' has been installed. Read more in ./gnovm/\033[0m" + @printf "\033[0;32m[+] 'gno' has been installed. Read more in ./gnovm/\033[0m\n" .PHONY: install.gnodev install.gnodev: $(MAKE) --no-print-directory -C ./contribs install.gnodev - @echo "\033[0;32m[+] 'gnodev' has been installed. Read more in ./contribs/gnodev/\033[0m" + @printf "\033[0;32m[+] 'gnodev' has been installed. Read more in ./contribs/gnodev/\033[0m\n" # old aliases .PHONY: install_gnokey diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 34f6d7a9552..30196527311 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -64,7 +64,7 @@ var defaultDevOptions = &devCfg{ chainId: "dev", maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", - nodeRPCListenerAddr: "127.0.0.1:36657", + nodeRPCListenerAddr: "127.0.0.1:26657", deployKey: DefaultDeployerAddress.String(), home: gnoenv.HomeDir(), root: gnoenv.RootDir(), @@ -84,9 +84,7 @@ func main() { Name: "gnodev", ShortUsage: "gnodev [flags] [path ...]", ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", - LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface -primarily for realm package development. It automatically loads the 'examples' directory and any -additional specified paths.`, + LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface primarily for realm package development. It automatically loads the 'examples' directory and any additional specified paths.`, }, cfg, func(_ context.Context, args []string) error { diff --git a/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start-manual.gif b/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start-manual.gif new file mode 100644 index 00000000000..40c79bd91f6 Binary files /dev/null and b/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start-manual.gif differ diff --git a/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start.gif b/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start.gif index 4da21bc863b..4882aabdfde 100644 Binary files a/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start.gif and b/docs/assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start.gif differ diff --git a/docs/concepts/stdlibs/gnopher-hole.md b/docs/concepts/stdlibs/gnopher-hole.md index b8795cc5af7..b9ce0c700af 100644 --- a/docs/concepts/stdlibs/gnopher-hole.md +++ b/docs/concepts/stdlibs/gnopher-hole.md @@ -12,7 +12,7 @@ in Go. There are generally three reasons why a function should be natively defined: 1. It relies on inspecting the Gno Virtual Machine itself, i.e. `std.AssertOriginCall` - or `std.CurrentRealmPath`. + or `std.CurrentRealm`. 2. It relies on `unsafe`, or other features which are not planned to be available in the GnoVM, i.e. `math.Float64frombits`. 3. Its native Go performance significantly outperforms the Gno counterpart by diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index 8546ef44df2..7f0734cdc28 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -40,7 +40,8 @@ state - they are refreshed with every new commit to the `master` branch. monorepo For more information on the Portal Loop, and how it can be best utilized, -check out the [Portal Loop concept page](./portal-loop.md). +check out the [Portal Loop concept page](./portal-loop.md). Also, you can find +the Portal Loop faucet on [`gno.land/faucet`](https://gno.land/faucet). ## Staging Staging is a testnet that is reset once every 60 minutes. diff --git a/docs/getting-started/local-setup/setting-up-a-local-chain.md b/docs/getting-started/local-setup/setting-up-a-local-chain.md index 23b4e110820..30360333208 100644 --- a/docs/getting-started/local-setup/setting-up-a-local-chain.md +++ b/docs/getting-started/local-setup/setting-up-a-local-chain.md @@ -13,13 +13,13 @@ Additionally, you will see the different options you can use to make your Gno in - [`gnoland` installed](local-setup.md#3-installing-other-gno-tools). -## Starting a node with a default configuration +## Starting a local node (lazy init) You can start a Gno blockchain node with the default configuration by navigating to the `gno.land` sub-folder and running the following command: ```bash -gnoland start +gnoland start --lazy ``` The command will trigger a chain initialization process (if you haven't run the node before), and start the Gno node, @@ -27,6 +27,17 @@ which is ready to accept transactions and interact with other Gno nodes. ![gnoland start](../../assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start.gif) +:::info Lazy init + +Starting a Gno blockchain node using just the `gnoland start --lazy` command implies a few things: + +- the default configuration will be used, and generated on disk in the `gnoland-data` directory +- random secrets data will be generated (node private keys, networking keys...) +- an entirely new `genesis.json` will be used, and generated on disk in the `../gnoland-data` directory. The genesis + will have a single validator, whose public key is derived from the previously generated node secrets + +::: + To view the command defaults, simply run the `help` command: ```bash @@ -37,10 +48,9 @@ Let's break down the most important default settings: - `chainid` - the ID of the Gno chain. This is used for Gno clients, and distinguishing the chain from other Gno chains (ex. through IBC) -- `config` - the custom node configuration file - for more details on utilizing this file - `genesis-balances-file` - the initial premine balances file, which contains initial native currency allocations for - the chain. By default, the genesis balances file is located in `gno.land/genesis/genesis_balances.txt`, this is also the + the chain. By default, the genesis balances file is located in `gno.land/genesis/genesis_balances.txt`, this is also + the reason why we need to navigate to the `gno.land` sub-folder to run the command with default settings - `data-dir` - the working directory for the node configuration and node data (state DB) @@ -48,15 +58,230 @@ Let's break down the most important default settings: As mentioned, the working directory for the node is located in `data-dir`. To reset the chain, you need to delete this directory and start the node up again. If you are using the default node configuration, you can run -`make fclean` from the `gno.land` sub-folder to delete the `tempdir` working directory. +`make fclean` from the `gno.land` sub-folder to delete the `gnoland-data` working directory. + +::: + +## Starting a local node (manual configuration) + +Manually configuring and starting the Gno blockchain node is a bit more involved than simply initializing it "lazily", +and involves the following steps: + +- generating the node secrets, and configuration +- generating the `genesis.json`, and populating it +- starting the node with the generated data + +### 1. Generate the node directory (secrets + config) + +You can generate the default node directory secrets using the following command: + +```shell +gnoland secrets init +``` + +And generate the default node config using the following command: + +```shell +gnoland config init +``` + +This will initialize the following directory structure: + +```shell +. +└── gnoland-data/ + ├── secrets/ + │ ├── priv_validator_state.json + │ ├── node_key.json + │ └── priv_validator_key.json + └── config/ + └── config.toml +``` + +A couple of things to note: + +- `gnoland config init` initializes a default configuration +- `gnoland secrets init` initializes new node secrets (validator key, node p2p key) + +Essentially, `gnoland start --lazy` is simply a combination of `gnoland secrets generate` and `gnoland config generate`, +with the default options enabled. + +#### Changing the node configuration + +To change the configuration params, such as for example the node's listen address, you can utilize the following +command: + +```shell +gnoland config set rpc.laddr tcp://0.0.0.0:26657 +``` + +This will update the RPC listen address to `0.0.0.0:26657`. You can verify the configuration was updated by running: + +```go +gnoland config get rpc.laddr +``` + +### 2. Generate the `genesis.json` + +:::info Where's the `genesis.json`? + +In this example, we are starting a completely new network. In case you are connecting to an existing network, you don't +need to regenerate the `genesis.json`, but simply fetch it from publicly available resources of the Gno chain you're +trying to connect to. ::: -## Changing the chain ID +Generating an empty `genesis.json` is relatively straightforward: + +```shell +gnoland genesis generate +``` + +The resulting `genesis.json` is empty: + +```json +{ + "genesis_time": "2024-05-08T10:25:09Z", + "chain_id": "dev", + "consensus_params": { + "Block": { + "MaxTxBytes": "1000000", + "MaxDataBytes": "2000000", + "MaxBlockBytes": "0", + "MaxGas": "10000000", + "TimeIotaMS": "100" + }, + "Validator": { + "PubKeyTypeURLs": [ + "/tm.PubKeyEd25519" + ] + } + }, + "app_hash": null +} +``` + +This will generate a `genesis.json` in the calling directory, by default. To check all configurable options when +generating the `genesis.json`, you can run the command using the `--help` flag: -:::info Changing the Gno chain ID has several implications +```shell +gnoland genesis generate --help -- It affects how the Gno node communicates with other Gno nodes / chains +USAGE + generate [flags] + +Generates a node's genesis.json based on specified parameters + +FLAGS + -block-max-data-bytes 2000000 the max size of the block data + -block-max-gas 10000000 the max gas limit for the block + -block-max-tx-bytes 1000000 the max size of the block transaction + -block-time-iota 100 the block time iota (in ms) + -chain-id dev the ID of the chain + -genesis-time 1715163944 the genesis creation time. Defaults to current time + -output-path ./genesis.json the output path for the genesis.json +``` + +### 3. Add the initial validator set + +A new Gno chain cannot advance without an active validator set. +Since this example follows starting a completely new Gno chain, you need to add at least one validator to the validator +set. + +Luckily, we've generated the node secrets in step #1 -- we will utilize the generated node key, so the process we start +locally will be the validator node for the new Gno network. + +To display the generated node key data, run the following command: + +```shell +gnoland secrets get ValidatorPrivateKey +``` + +This will display the information we need for updating the `genesis.json`: + +```shell +[Validator Key Info] + +Address: g10e3smsmusjn00n7j75fk9u4zta8djrlglcv6af +Public Key: gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqhjhrqd7xlhda7spfdtx6lrcjxlk67av46w7eng9z4e2ch478fsk4xmq3j +``` + +Updating the `genesis.json` is relatively simple, running the following command will add the generated node info to the +validator set: + +```shell +gnoland genesis validator add \ +--address g10e3smsmusjn00n7j75fk9u4zta8djrlglcv6af \ +--pub-key gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqhjhrqd7xlhda7spfdtx6lrcjxlk67av46w7eng9z4e2ch478fsk4xmq3j \ +--name Cuttlas +``` + +We can verify that the new validator was indeed added to the validator set: + +```json +{ + "genesis_time": "2024-05-08T10:25:09Z", + "chain_id": "dev", + "consensus_params": { + "Block": { + "MaxTxBytes": "1000000", + "MaxDataBytes": "2000000", + "MaxBlockBytes": "0", + "MaxGas": "10000000", + "TimeIotaMS": "100" + }, + "Validator": { + "PubKeyTypeURLs": [ + "/tm.PubKeyEd25519" + ] + } + }, + "validators": [ + { + "address": "g1lz2ez3ceeds9f6jllwy7u0hvkphuuv0plcc8pp", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "AvaVf/cH84urHNuS1lo3DYmtEErxkTLRsrcr71QoAr4=" + }, + "power": "1", + "name": "Cuttlas" + } + ], + "app_hash": null +} +``` + +### 4. Starting the chain + +We have completed the main aspects of setting up a node: + +- generated the node directory (secrets and configuration) ✅ +- generated a `genesis.json` ✅ +- added an initial validator set to the `genesis.json` ✅ + +Now, we can go ahead and start the Gno chain for the first time, by running: + +```shell +gnoland start \ +--genesis ./genesis.json \ +--data-dir ./gnoland-data +``` + +That's it! 🎉 + +Your new Gno node (chain) should be up and running: + +![gnoland start](../../assets/getting-started/local-setup/setting-up-a-local-chain/gnoland-start-manual.gif) + +## Chain runtime options + +### Changing the chain ID + +:::info Changing the Gno chain ID + +Below are some implications to consider when changing the chain ID: + +- it affects how the Gno node communicates with other Gno nodes / chains - Gno clients that communicate through JSON-RPC need to match this value It's important to configure your node properly before launching it in a distributed network. @@ -109,7 +334,7 @@ have no effect. ::: -## Changing the node configuration +### Changing the node configuration You can specify a node configuration file using the `--config` flag. @@ -117,7 +342,7 @@ You can specify a node configuration file using the `--config` flag. gnoland start --config config.toml ``` -## Changing the premine list +### Changing the premine list You do not need to use the `gno.land/genesis/genesis_balances.txt` file as the source of truth for initial network funds. @@ -140,3 +365,11 @@ Following this pattern, potential entries into the genesis balances file would l g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt=10000000000ugnot g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot ``` + +:::info Genesis generation + +Genesis block generation happens only once during the lifetime of a Gno chain. +This means that if you specify a balances file using `gnoland start`, and the chain has already started (advanced from +block 0), the specified balance sheet will not be applied. + +::: \ No newline at end of file diff --git a/docs/gno-tooling/cli/gnokey.md b/docs/gno-tooling/cli/gnokey.md index 8479e9c112d..77c25563fdd 100644 --- a/docs/gno-tooling/cli/gnokey.md +++ b/docs/gno-tooling/cli/gnokey.md @@ -190,7 +190,7 @@ gnokey maketx addpkg \ ### `call` -This subcommand lets you call a public function. +This subcommand lets you call any exported function. ```bash # Register @@ -207,6 +207,20 @@ gnokey maketx call \ > unsigned.tx ``` +:::warning `call` is a state-changing message + +All exported functions, including `Render()`, can be called in two main ways: +`call` and [`query vm/qeval`](#query). + +With `call`, any state change that happened in the function being called will be +applied and persisted in on the blockchain, and the gas used for this call will +be subtracted from the caller balance. + +As opposed to this, an ABCI query, such as `vm/qeval` will not persist state +changes and does not cost gas, only evaluating the expression in read-only mode. + +::: + #### **SignBroadcast Options** | Name | Type | Description | diff --git a/docs/gno-tooling/cli/gnoland.md b/docs/gno-tooling/cli/gnoland.md index 9bd722b0f2c..3d4ba9eac00 100644 --- a/docs/gno-tooling/cli/gnoland.md +++ b/docs/gno-tooling/cli/gnoland.md @@ -4,28 +4,72 @@ id: gno-tooling-gnoland # gnoland -## Run a Gnoland Node +## Overview -Start a node on the Gnoland blockchain with the following command. +`gnoland` is the Gno.land blockchain client binary, which is capable of managing node working files, as well +as starting the blockchain client itself. -```bash -gnoland +## `gnoland init` + +`gnoland init` is supposed to initialize the node's working directory in the given path. The node's data directory is +comprised initially from the node's secrets and config (default values). + +It is meant to be an initial step in starting the gno blockchain client, as the client itself cannot run without secrets +data like private keys, and a configuration. When the blockchain client is started, it will initialize on its own +relevant DB working directories inside the node directory. + +```shell +gnoland init --help + +USAGE + init [flags] + +initializes the node directory containing the secrets and configuration files + +FLAGS + -data-dir gnoland-data the path to the node's data directory + -force=false overwrite existing data, if any +``` + +### Example usage + +#### Generating fresh secrets / config + +To initialize the node secrets and configuration to `./example-node-data`, run the following command: + +```shell +gnoland init --data-dir ./example-node-data +``` + +This will initialize the following directory structure: + +```shell +. +└── example-node-data/ + ├── secrets/ + │ ├── priv_validator_state.json + │ ├── node_key.json + │ └── priv_validator_key.json + └── config/ + └── config.toml ``` -### **Sub Commands** -| Command | Description | -| --------- | ----------------- | -| `start` | Run the full node | +#### Overwriting the secrets / config +In case there is an already existing node directory at the given path, you will need to provide an additional `--force` +flag to enable data overwrite. -### **Options** +:::warning Back up any secrets -| Name | Type | Description | -|----------------------------| ------- | --------------------------------------------------------------------------------------- | -| `chainid` | String | The id of the chain (default: `dev`). | -| `genesis-balances-file` | String | The initial GNOT distribution file (default: `./gnoland/genesis/genesis_balances.txt`). | -| `genesis-remote` | String | Replacement '%%REMOTE%%' in genesis (default: `"localhost:26657"`). | -| `genesis-txs-file` | String | Initial txs to be executed (default: `"./gnoland/genesis/genesis_txs.jsonl"`). | -| `data-dir` | String | directory for config and data (default: `gnoland-data`). | -| `skip-failing-genesis-txs` | Boolean | Skips transactions that fail from the `genesis-txs-file` | -| `skip-start` | Boolean | Quits after initialization without starting the node. | +Running `gnoland init` will generate completely new node secrets (validator private key, node p2p key), so make sure +you back up any existing secrets (located at `/secrets`) if you intend to overwrite them, in case you don't +want to lose them. + +::: + +Following up from the previous example where our desired node directory is `example-node-data` - to +initialize a completely new node data directory, with overwriting any existing data, run the following command: + +```shell +gnoland init --data-dir ./example-node-data --force +``` diff --git a/docs/how-to-guides/interact-with-gnoland.md b/docs/how-to-guides/interact-with-gnoland.md index 527e20981a7..c4b2bd8c043 100644 --- a/docs/how-to-guides/interact-with-gnoland.md +++ b/docs/how-to-guides/interact-with-gnoland.md @@ -29,7 +29,7 @@ gnokey maketx call \ -gas-fee="1ugnot" \ -gas-wanted="5000000" \ -broadcast="true" \ - -remote="staging.gno.land:36657" \ + -remote="staging.gno.land:26657" \ -chainid="test3" \ -pkgpath="gno.land/r/demo/users" \ -func="Register" \ @@ -48,13 +48,13 @@ gnokey maketx call \ ```bash # Get account information -gnokey query -remote="staging.gno.land:36657" "auth/accounts/{address}" +gnokey query -remote="staging.gno.land:26657" "auth/accounts/{address}" # Get account balance -gnokey query -remote="staging.gno.land:36657" "bank/balances/{address}" +gnokey query -remote="staging.gno.land:26657" "bank/balances/{address}" # Get /r/demo/boards user information -gnokey query -remote="staging.gno.land:36657" -data "gno.land/r/demo/users +gnokey query -remote="staging.gno.land:26657" -data "gno.land/r/demo/users my_account" "vm/qrender" ``` @@ -68,7 +68,7 @@ gnokey maketx send \ -gas-fee="1ugnot" \ -gas-wanted="5000000" \ -broadcast="true" \ - -remote="staging.gno.land:36657" \ + -remote="staging.gno.land:26657" \ -chainid="test3" \ -to="{address}" \ # g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -send="{amount}{denom}" \ # 1234ugnot @@ -85,7 +85,7 @@ gnokey maketx call \ -gas-fee="1ugnot" \ -gas-wanted="5000000" \ -broadcast="true" \ - -remote "staging.gno.land:36657" \ + -remote "staging.gno.land:26657" \ -chainid="test3" \ -pkgpath="gno.land/r/demo/boards" \ -func="CreateBoard" \ diff --git a/docs/overview.md b/docs/overview.md index 2e96da17438..5db6fccfab1 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -10,7 +10,7 @@ version of the Go programming language called Gno." ## What is Gno.land? Gno.land is a Layer 1 blockchain platform that enables the execution of Smart Contracts using an interpreted -version of the Go programming language called Gno (Gno for short). +version of the Go programming language called Gno. ### Key Features and Technology diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 0d933db39f4..c2ec5409fc9 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -9,7 +9,7 @@ id: network-config | Portal Loop | https://rpc.gno.land:443 | `portal-loop` | | Testnet 4 | upcoming | upcoming | | Testnet 3 | https://rpc.test3.gno.land:443 | `test3` | -| Staging | https://rpc.staging.gno.land:36657 | `test3` | +| Staging | https://rpc.staging.gno.land:26657 | `test3` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 06c40d63afc..f3dddaba938 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -41,18 +41,6 @@ std.Emit("MyEvent", "myKey1", "myValue1", "myKey2", "myValue2") ``` --- -## CurrentRealmPath -```go -func CurrentRealmPath() string -``` -Returns the path of the realm it is called in. - -#### Usage -```go -realmPath := std.CurrentRealmPath() // gno.land/r/demo/users -``` ---- - ## GetChainID ```go func GetChainID() string diff --git a/docs/reference/stdlibs/std/coin.md b/docs/reference/stdlibs/std/coin.md index f2e1cf694b4..3675e1365f6 100644 --- a/docs/reference/stdlibs/std/coin.md +++ b/docs/reference/stdlibs/std/coin.md @@ -10,31 +10,167 @@ type Coin struct { Denom string `json:"denom"` Amount int64 `json:"amount"` } + +func NewCoin(denom string, amount int64) Coin {...} func (c Coin) String() string {...} func (c Coin) IsGTE(other Coin) bool {...} +func (c Coin) IsLT(other Coin) bool {...} +func (c Coin) IsEqual(other Coin) bool {...} +func (c Coin) Add(other Coin) Coin {...} +func (c Coin) Sub(other Coin) Coin {...} +func (c Coin) IsPositive() bool {...} +func (c Coin) IsNegative() bool {...} +func (c Coin) IsZero() bool {...} ``` +## NewCoin +Returns a new Coin with a specific denomination and amount. + +#### Usage +```go +coin := std.NewCoin("ugnot", 100) +``` +--- + ## String Returns a string representation of the `Coin` it was called upon. #### Usage ```go -coin := std.Coin{"ugnot", 100} +coin := std.NewCoin("ugnot", 100) coin.String() // 100ugnot ``` --- + ## IsGTE -Checks if the amount of `other` Coin is greater or equal than amount of Coin `c` it was called upon. -If coins compared are not of the same denomination, `IsGTE` will panic. +Checks if the amount of `other` Coin is greater than or equal than amount of +Coin `c` it was called upon. If coins compared are not of the same denomination, +`IsGTE` will panic. #### Parameters - `other` **Coin** to compare with #### Usage ```go -coin1 := std.Coin{"ugnot", 150} -coin2 := std.Coin{"ugnot", 100} +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", 100) coin1.IsGTE(coin2) // true coin2.IsGTE(coin1) // false -``` \ No newline at end of file +``` +--- + +## IsLT +Checks if the amount of `other` Coin is less than the amount of Coin `c` it was +called upon. If coins compared are not of the same denomination, `IsLT` will +panic. + +#### Parameters +- `other` **Coin** to compare with + +#### Usage +```go +coin := std.NewCoin("ugnot", 150) +coin := std.NewCoin("ugnot", 100) + +coin1.IsLT(coin2) // false +coin2.IsLT(coin1) // true +``` +--- + +## IsEqual +Checks if the amount of `other` Coin is equal to the amount of Coin `c` it was +called upon. If coins compared are not of the same denomination, `IsEqual` will +panic. + +#### Parameters +- `other` **Coin** to compare with + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", 100) +coin3 := std.NewCoin("ugnot", 100) + +coin1.IsEqual(coin2) // false +coin2.IsEqual(coin1) // false +coin2.IsEqual(coin3) // true +``` +--- + +## Add +Adds two coins of the same denomination. If coins are not of the same +denomination, `Add` will panic. If final amount is larger than the maximum size +of `int64`, `Add` will panic with an overflow error. Adding a negative amount +will result in subtraction. + +#### Parameters +- `other` **Coin** to add + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", 100) + +coin3 := coin1.Add(coin2) +coin3.String() // 250ugnot +``` +--- + +## Sub +Subtracts two coins of the same denomination. If coins are not of the same +denomination, `Sub` will panic. If final amount is smaller than the minimum size +of `int64`, `Sub` will panic with an underflow error. Subtracting a negative amount +will result in addition. + +#### Parameters +- `other` **Coin** to subtract + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", 100) + +coin3 := coin1.Sub(coin2) +coin3.String() // 50ugnot +``` +--- + +## IsPositive +Checks if a coin amount is positive. + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", -150) + +coin1.IsPositive() // true +coin2.IsPositive() // false +``` +--- + +## IsNegative +Checks if a coin amount is negative. + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", -150) + +coin1.IsNegative() // false +coin2.IsNegative() // true +``` +--- + +## IsZero +Checks if a coin amount is zero. + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("ugnot", 0) + +coin1.IsZero() // false +coin2.IsZero() // true +``` + diff --git a/docs/reference/stdlibs/std/coins.md b/docs/reference/stdlibs/std/coins.md index 0f813178330..ea38ce51580 100644 --- a/docs/reference/stdlibs/std/coins.md +++ b/docs/reference/stdlibs/std/coins.md @@ -8,11 +8,28 @@ id: coins ```go type Coins []Coin + +func NewCoins(coins ...Coin) Coins {...} func (c Coins) String() string {...} func (c Coins) AmountOf(denom string) int64 {...} func (c Coins) Add(other Coins) Coins {...} ``` +### NewCoins +Returns a new set of `Coins` given one or more `Coin`. Consolidates any denom +duplicates into one, keeping the properties of a mathematical set. + +#### Usage +```go +coin1 := std.NewCoin("ugnot", 150) +coin2 := std.NewCoin("example", 100) +coin3 := std.NewCoin("ugnot", 100) + +coins := std.NewCoins(coin1, coin2, coin3) +coins.String() // 250ugnot, 100example +``` +--- + ### String Returns a string representation of the `Coins` set it was called upon. diff --git a/docs/reference/stdlibs/std/testing.md b/docs/reference/stdlibs/std/testing.md index 8c9146c81a1..102b9ed6d70 100644 --- a/docs/reference/stdlibs/std/testing.md +++ b/docs/reference/stdlibs/std/testing.md @@ -5,7 +5,6 @@ id: testing # Testing ```go -func TestCurrentRealm() string func TestSkipHeights(count int64) func TestSetOrigCaller(addr Address) func TestSetOrigPkgAddr(addr Address) @@ -13,16 +12,6 @@ func TestSetOrigSend(sent, spent Coins) func TestIssueCoins(addr Address, coins Coins) ``` -## TestCurrentRealm -```go -func TestCurrentRealm() string -``` -Returns the current realm path. - -#### Usage -```go -currentRealmPath := std.TestCurrentRealm() -``` --- ## TestSkipHeights diff --git a/docs/reference/tm2-js-client/Provider/json-rpc-provider.md b/docs/reference/tm2-js-client/Provider/json-rpc-provider.md index b7700e1d97c..50d816e89f9 100644 --- a/docs/reference/tm2-js-client/Provider/json-rpc-provider.md +++ b/docs/reference/tm2-js-client/Provider/json-rpc-provider.md @@ -17,6 +17,6 @@ Creates a new instance of the JSON-RPC Provider #### Usage ```ts -new JSONRPCProvider('http://staging.gno.land:36657'); +new JSONRPCProvider('http://staging.gno.land:26657'); // provider is created ``` diff --git a/docs/reference/tm2-js-client/Provider/ws-provider.md b/docs/reference/tm2-js-client/Provider/ws-provider.md index ef91f45d4e2..9d4f2390c28 100644 --- a/docs/reference/tm2-js-client/Provider/ws-provider.md +++ b/docs/reference/tm2-js-client/Provider/ws-provider.md @@ -18,7 +18,7 @@ Creates a new instance of the WebSocket Provider #### Usage ```ts -new WSProvider('ws://staging.gno.land:36657/ws'); +new WSProvider('ws://staging.gno.land:26657/ws'); // provider with WS connection is created ``` @@ -30,7 +30,7 @@ with the WS provider #### Usage ```ts -const wsProvider = new WSProvider('ws://staging.gno.land:36657/ws'); +const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); wsProvider.closeConnection(); // WS connection is now closed @@ -52,7 +52,7 @@ Returns **Promise>** ```ts const request: RPCRequest = // ... -const wsProvider = new WSProvider('ws://staging.gno.land:36657/ws'); +const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); wsProvider.sendRequest(request); // request is sent over the open WS connection @@ -73,7 +73,7 @@ Returns **Result** ```ts const response: RPCResponse = // ... -const wsProvider = new WSProvider('ws://staging.gno.land:36657/ws'); +const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); wsProvider.parseResponse(response); // response is parsed @@ -88,7 +88,7 @@ Returns **Promise** #### Usage ```ts -const wsProvider = new WSProvider('ws://staging.gno.land:36657/ws'); +const wsProvider = new WSProvider('ws://staging.gno.land:26657/ws'); await wsProvider.waitForOpenConnection() // status of the connection is: CONNECTED diff --git a/examples/gno.land/p/demo/tests/tests.gno b/examples/gno.land/p/demo/tests/tests.gno index 1a2c2526d01..43732d82dac 100644 --- a/examples/gno.land/p/demo/tests/tests.gno +++ b/examples/gno.land/p/demo/tests/tests.gno @@ -18,7 +18,7 @@ func IncCounter() { } func CurrentRealmPath() string { - return std.CurrentRealmPath() + return std.CurrentRealm().PkgPath() } //---------------------------------------- diff --git a/examples/gno.land/r/demo/boards/README.md b/examples/gno.land/r/demo/boards/README.md index 849998b04e9..6e3dac115b1 100644 --- a/examples/gno.land/r/demo/boards/README.md +++ b/examples/gno.land/r/demo/boards/README.md @@ -8,7 +8,7 @@ name ["gno.land/r/demo/boards"](https://gno.land/r/demo/boards/) ## Build `gnokey`, create your account, and interact with Gno. NOTE: Where you see `-remote localhost:26657` here, that flag can be replaced -with `-remote test3.gno.land:36657` if you have $GNOT on the testnet. +with `-remote test3.gno.land:26657` if you have $GNOT on the testnet. (To use the testnet, also replace `-chainid dev` with `-chainid test3` .) ### Build `gnokey` (and other tools). diff --git a/examples/gno.land/r/demo/boards/public.gno b/examples/gno.land/r/demo/boards/public.gno index 6643c863cec..1ef2e72f4c2 100644 --- a/examples/gno.land/r/demo/boards/public.gno +++ b/examples/gno.land/r/demo/boards/public.gno @@ -33,7 +33,7 @@ func CreateBoard(name string) BoardID { func checkAnonFee() bool { sent := std.GetOrigSend() - anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} + anonFeeCoin := std.NewCoin("ugnot", int64(gDefaultAnonFee)) if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { return true } diff --git a/examples/gno.land/r/demo/event/event.gno b/examples/gno.land/r/demo/event/event.gno new file mode 100644 index 00000000000..9e5de540734 --- /dev/null +++ b/examples/gno.land/r/demo/event/event.gno @@ -0,0 +1,9 @@ +package event + +import ( + "std" +) + +func Emit(value string) { + std.Emit("TAG", "key", value) +} diff --git a/examples/gno.land/r/demo/event/gno.mod b/examples/gno.land/r/demo/event/gno.mod new file mode 100644 index 00000000000..64987d43d79 --- /dev/null +++ b/examples/gno.land/r/demo/event/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/event diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 2062df6903d..14adad7355c 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -17,7 +17,7 @@ func Counter() int { } func CurrentRealmPath() string { - return std.CurrentRealmPath() + return std.CurrentRealm().PkgPath() } var initOrigCaller = std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 4fe486a71d0..5b4b8ec2c14 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -35,7 +35,7 @@ func Register(inviter std.Address, name string, profile string) { panic("should not happen") // because std.AssertOrigCall(). } sentCoins := std.GetOrigSend() - minCoin := std.Coin{"ugnot", minFee} + minCoin := std.NewCoin("ugnot", minFee) if inviter == "" { // banker := std.GetBanker(std.BankerTypeOrigSend) if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) { diff --git a/examples/gno.land/r/demo/users/z_0_filetest.gno b/examples/gno.land/r/demo/users/z_0_filetest.gno index 45d789b1a3e..cbb2e9209f4 100644 --- a/examples/gno.land/r/demo/users/z_0_filetest.gno +++ b/examples/gno.land/r/demo/users/z_0_filetest.gno @@ -7,10 +7,10 @@ import ( ) func main() { - std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) + std.TestSetOrigSend(std.Coins{std.NewCoin("dontcare", 1)}, nil) users.Register("", "gnouser", "my profile") println("done") } // Error: -// invalid coin denominations: dontcare +// incompatible coin denominations: dontcare, ugnot diff --git a/examples/gno.land/r/gnoland/faucet/admin.gno b/examples/gno.land/r/gnoland/faucet/admin.gno index a4916c5af02..37108059b74 100644 --- a/examples/gno.land/r/gnoland/faucet/admin.gno +++ b/examples/gno.land/r/gnoland/faucet/admin.gno @@ -25,7 +25,7 @@ func AdminSetTransferLimit(amount int64) string { if err := assertIsAdmin(); err != nil { return err.Error() } - gLimit = std.Coin{Denom: "ugnot", Amount: amount} + gLimit = std.NewCoin("ugnot", amount) return "" } diff --git a/examples/gno.land/r/gnoland/faucet/faucet.gno b/examples/gno.land/r/gnoland/faucet/faucet.gno index 038c7283fa1..908b86d4aaf 100644 --- a/examples/gno.land/r/gnoland/faucet/faucet.gno +++ b/examples/gno.land/r/gnoland/faucet/faucet.gno @@ -21,7 +21,7 @@ var ( gTotalTransfers = uint(0) // per request limit, 350 gnot - gLimit std.Coin = std.Coin{"ugnot", 350000000} + gLimit std.Coin = std.NewCoin("ugnot", 350000000) ) func Transfer(to std.Address, send int64) string { @@ -37,7 +37,7 @@ func Transfer(to std.Address, send int64) string { if send > gLimit.Amount { return errors.New("Per request limit " + gLimit.String() + " exceed").Error() } - sendCoins := std.Coins{std.Coin{Denom: "ugnot", Amount: send}} + sendCoins := std.Coins{std.NewCoin("ugnot", send)} gTotalTransferred = gTotalTransferred.Add(sendCoins) gTotalTransfers++ diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index d8bec8242c2..881b9bd104e 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -245,6 +245,7 @@ func discoverLinks() ui.Element { - [Visit the official documentation](https://docs.gno.land) - [Gno by Example](https://gno-by-example.com/) - [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev) +- [Get testnet GNOTs](https://faucet.gno.land)
diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index fa6fbe4e2a1..e67f9ff5fdc 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -43,6 +43,7 @@ func main() { //- [Visit the official documentation](https://docs.gno.land) //- [Gno by Example](https://gno-by-example.com/) //- [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev) +//- [Get testnet GNOTs](https://faucet.gno.land) // //
//
diff --git a/gno.land/cmd/gnoland/config.go b/gno.land/cmd/gnoland/config.go index 22d23e3ecb1..8e11e57fd5f 100644 --- a/gno.land/cmd/gnoland/config.go +++ b/gno.land/cmd/gnoland/config.go @@ -3,9 +3,11 @@ package main import ( "flag" "fmt" + "path/filepath" "reflect" "strings" + "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -39,11 +41,21 @@ func (c *configCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.configPath, "config-path", - "./config.toml", + constructConfigPath(defaultNodeDir), "the path for the config.toml", ) } +// constructConfigPath constructs the default config path, using +// the given node directory +func constructConfigPath(nodeDir string) string { + return filepath.Join( + nodeDir, + config.DefaultConfigDir, + config.DefaultConfigFileName, + ) +} + // getFieldAtPath fetches the given field from the given path func getFieldAtPath(currentValue reflect.Value, path []string) (*reflect.Value, error) { // Look at the current section, and figure out if diff --git a/gno.land/cmd/gnoland/config_init.go b/gno.land/cmd/gnoland/config_init.go index be7902b48cd..238db6bd348 100644 --- a/gno.land/cmd/gnoland/config_init.go +++ b/gno.land/cmd/gnoland/config_init.go @@ -3,17 +3,27 @@ package main import ( "context" "errors" + "flag" "fmt" + "os" + "path/filepath" "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/commands" + osm "github.com/gnolang/gno/tm2/pkg/os" ) var errInvalidConfigOutputPath = errors.New("invalid config output path provided") +type configInitCfg struct { + configCfg + + forceOverwrite bool +} + // newConfigInitCmd creates the config init command func newConfigInitCmd(io commands.IO) *commands.Command { - cfg := &configCfg{} + cfg := &configInitCfg{} cmd := commands.NewCommand( commands.Metadata{ @@ -32,15 +42,36 @@ func newConfigInitCmd(io commands.IO) *commands.Command { return cmd } -func execConfigInit(cfg *configCfg, io commands.IO) error { +func (c *configInitCfg) RegisterFlags(fs *flag.FlagSet) { + c.configCfg.RegisterFlags(fs) + + fs.BoolVar( + &c.forceOverwrite, + "force", + false, + "overwrite existing config.toml, if any", + ) +} + +func execConfigInit(cfg *configInitCfg, io commands.IO) error { // Check the config output path if cfg.configPath == "" { return errInvalidConfigOutputPath } + // Make sure overwriting the config is enabled + if osm.FileExists(cfg.configPath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + // Get the default config c := config.DefaultConfig() + // Make sure the path is created + if err := os.MkdirAll(filepath.Dir(cfg.configPath), 0o755); err != nil { + return fmt.Errorf("unable to create config dir, %w", err) + } + // Save the config to the path if err := config.WriteConfigFile(cfg.configPath, c); err != nil { return fmt.Errorf("unable to initialize config, %w", err) diff --git a/gno.land/cmd/gnoland/config_init_test.go b/gno.land/cmd/gnoland/config_init_test.go index c576aec1641..03a6d40a239 100644 --- a/gno.land/cmd/gnoland/config_init_test.go +++ b/gno.land/cmd/gnoland/config_init_test.go @@ -58,4 +58,73 @@ func TestConfig_Init(t *testing.T) { assert.NoError(t, cfg.ValidateBasic()) assert.Equal(t, cfg, config.DefaultConfig()) }) + + t.Run("unable to overwrite config", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + path := filepath.Join(tempDir, "config.toml") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "init", + "--config-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the config is valid + cfg, err := config.LoadConfigFile(path) + require.NoError(t, err) + + assert.NoError(t, cfg.ValidateBasic()) + assert.Equal(t, cfg, config.DefaultConfig()) + + // Try to initialize again, expecting failure + cmd = newRootCmd(commands.NewTestIO()) + + cmdErr = cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errOverwriteNotEnabled) + }) + + t.Run("config overwritten", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + path := filepath.Join(tempDir, "config.toml") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "init", + "--force", + "--config-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the config is valid + cfg, err := config.LoadConfigFile(path) + require.NoError(t, err) + + assert.NoError(t, cfg.ValidateBasic()) + assert.Equal(t, cfg, config.DefaultConfig()) + + // Try to initialize again, expecting success + cmd = newRootCmd(commands.NewTestIO()) + + cmdErr = cmd.ParseAndRun(context.Background(), args) + assert.NoError(t, cmdErr) + }) } diff --git a/gno.land/cmd/gnoland/genesis_validator.go b/gno.land/cmd/gnoland/genesis_validator.go index cf619ac40dc..91d3e4af7dd 100644 --- a/gno.land/cmd/gnoland/genesis_validator.go +++ b/gno.land/cmd/gnoland/genesis_validator.go @@ -44,6 +44,6 @@ func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) { &c.address, "address", "", - "the output path for the genesis.json", + "the gno bech32 address of the validator", ) } diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index d670151b358..8df716b1fed 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -33,9 +33,9 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newStartCmd(io), + newGenesisCmd(io), newSecretsCmd(io), newConfigCmd(io), - newGenesisCmd(io), ) return cmd diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go index 36113a3e207..fda2a664c28 100644 --- a/gno.land/cmd/gnoland/secrets.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -3,7 +3,9 @@ package main import ( "errors" "flag" + "path/filepath" + "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -13,7 +15,6 @@ var ( ) const ( - defaultSecretsDir = "./secrets" defaultValidatorKeyName = "priv_validator_key.json" defaultNodeKeyName = "node_key.json" defaultValidatorStateName = "priv_validator_state.json" @@ -58,7 +59,16 @@ func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.dataDir, "data-dir", - defaultSecretsDir, + constructSecretsPath(defaultNodeDir), "the secrets output directory", ) } + +// constructSecretsPath constructs the default secrets path, using +// the given node directory +func constructSecretsPath(nodeDir string) string { + return filepath.Join( + nodeDir, + config.DefaultSecretsDir, + ) +} diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 55be73c22fc..85181d4735c 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -88,55 +88,54 @@ func execSecretsInit(cfg *secretsInitCfg, args []string, io commands.IO) error { switch key { case validatorPrivateKeyKey: if osm.FileExists(validatorKeyPath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled + return fmt.Errorf("unable to overwrite validator key, %w", errOverwriteNotEnabled) } // Initialize and save the validator's private key return initAndSaveValidatorKey(validatorKeyPath, io) case nodeKeyKey: if osm.FileExists(nodeKeyPath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled + return fmt.Errorf("unable to overwrite the node' p2p key, %w", errOverwriteNotEnabled) } // Initialize and save the node's p2p key return initAndSaveNodeKey(nodeKeyPath, io) case validatorStateKey: if osm.FileExists(validatorStatePath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled + return fmt.Errorf("unable to overwrite validator last sign state, %w", errOverwriteNotEnabled) } // Initialize and save the validator's last sign state return initAndSaveValidatorState(validatorStatePath, io) default: - // Check if the validator key should be overwritten - if osm.FileExists(validatorKeyPath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled - } - - // Check if the validator state should be overwritten - if osm.FileExists(validatorStatePath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled - } - - // Check if the node key should be overwritten - if osm.FileExists(nodeKeyPath) && !cfg.forceOverwrite { - return errOverwriteNotEnabled - } - // No key provided, initialize everything - // Initialize and save the validator's private key - if err := initAndSaveValidatorKey(validatorKeyPath, io); err != nil { - return err - } - - // Initialize and save the validator's last sign state - if err := initAndSaveValidatorState(validatorStatePath, io); err != nil { - return err - } + return errors.Join( + overwriteGuard(validatorKeyPath, initAndSaveValidatorKey, cfg.forceOverwrite, io), + overwriteGuard(validatorStatePath, initAndSaveValidatorState, cfg.forceOverwrite, io), + overwriteGuard(nodeKeyPath, initAndSaveNodeKey, cfg.forceOverwrite, io), + ) + } +} - // Initialize and save the node's p2p key - return initAndSaveNodeKey(nodeKeyPath, io) +// overwriteGuard guards against unwanted secret overwrites, +// and executes the secret initialization if the secret is not present +func overwriteGuard( + path string, + initFn func(string, commands.IO) error, + overwriteEnabled bool, + io commands.IO, +) error { + // Check if the secret already exists + if osm.FileExists(path) && !overwriteEnabled { + return fmt.Errorf( + "unable to overwrite secret at %q, %w", + path, + errOverwriteNotEnabled, + ) } + + // Secret doesn't exist, initialize it + return initFn(path, io) } // initAndSaveValidatorKey generates a validator private key and saves it to the given path diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 11006d69246..282681edd63 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -5,8 +5,12 @@ import ( "errors" "flag" "fmt" + "io" + "os" + "os/signal" "path/filepath" "strings" + "syscall" "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -16,18 +20,20 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" - "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/null" - eventstorecfg "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/telemetry" + "go.uber.org/zap" "go.uber.org/zap/zapcore" ) +const defaultNodeDir = "gnoland-data" + +var errMissingGenesis = errors.New("missing genesis.json") + var startGraphic = strings.ReplaceAll(` __ __ ___ ____ ___ / /__ ____ ___/ / @@ -37,21 +43,17 @@ var startGraphic = strings.ReplaceAll(` `, "'", "`") type startCfg struct { - gnoRootDir string - skipFailingGenesisTxs bool - skipStart bool - genesisBalancesFile string - genesisTxsFile string + gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 genesisFile string chainID string - genesisRemote string dataDir string genesisMaxVMCycles int64 config string - - txEventStoreType string - txEventStorePath string - nodeConfigPath string + lazyInit bool logLevel string logFormat string @@ -64,11 +66,12 @@ func newStartCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "start", ShortUsage: "start [flags]", - ShortHelp: "run the full node", + ShortHelp: "starts the Gnoland blockchain node", + LongHelp: "Starts the Gnoland blockchain node, with accompanying setup", }, cfg, - func(_ context.Context, _ []string) error { - return execStart(cfg, io) + func(ctx context.Context, _ []string) error { + return execStart(ctx, cfg, io) }, ) } @@ -85,13 +88,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "don't panic when replaying invalid genesis txs", ) - fs.BoolVar( - &c.skipStart, - "skip-start", - false, - "quit after initialization, don't start the node", - ) - fs.StringVar( &c.genesisBalancesFile, "genesis-balances-file", @@ -130,7 +126,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.dataDir, "data-dir", - "gnoland-data", + defaultNodeDir, "the path to the node's data directory", ) @@ -155,36 +151,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "the flag config file (optional)", ) - fs.StringVar( - &c.nodeConfigPath, - "config-path", - "", - "the node TOML config file path (optional)", - ) - - fs.StringVar( - &c.txEventStoreType, - "tx-event-store-type", - null.EventStoreType, - fmt.Sprintf( - "type of transaction event store [%s]", - strings.Join( - []string{ - null.EventStoreType, - file.EventStoreType, - }, - ", ", - ), - ), - ) - - fs.StringVar( - &c.txEventStorePath, - "tx-event-store-path", - "", - fmt.Sprintf("path for the file tx event store (required if event store is '%s')", file.EventStoreType), - ) - fs.StringVar( &c.logLevel, "log-level", @@ -198,9 +164,16 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { log.ConsoleFormat.String(), "log format for the gnoland node", ) + + fs.BoolVar( + &c.lazyInit, + "lazy", + false, + "flag indicating if lazy init is enabled. Generates the node secrets, configuration, and genesis.json", + ) } -func execStart(c *startCfg, io commands.IO) error { +func execStart(ctx context.Context, c *startCfg, io commands.IO) error { // Get the absolute path to the node's data directory nodeDir, err := filepath.Abs(c.dataDir) if err != nil { @@ -213,103 +186,188 @@ func execStart(c *startCfg, io commands.IO) error { return fmt.Errorf("unable to get absolute path for the genesis.json, %w", err) } - var ( - cfg *config.Config - loadCfgErr error - ) - - // Set the node configuration - if c.nodeConfigPath != "" { - // Load the node configuration - // from the specified path - cfg, loadCfgErr = config.LoadConfigFile(c.nodeConfigPath) - } else { - // Load the default node configuration - cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(nodeDir) - } - - if loadCfgErr != nil { - return fmt.Errorf("unable to load node configuration, %w", loadCfgErr) - } - - // Initialize the log level - logLevel, err := zapcore.ParseLevel(c.logLevel) + // Initialize the logger + zapLogger, err := initializeLogger(io.Out(), c.logLevel, c.logFormat) if err != nil { - return fmt.Errorf("unable to parse log level, %w", err) + return fmt.Errorf("unable to initialize zap logger, %w", err) } - // Initialize the log format - logFormat := log.Format(strings.ToLower(c.logFormat)) - - // Initialize the zap logger - zapLogger := log.GetZapLoggerFn(logFormat)(io.Out(), logLevel) + defer func() { + // Sync the logger before exiting + _ = zapLogger.Sync() + }() // Wrap the zap logger logger := log.ZapLoggerToSlog(zapLogger) - // Initialize telemetry - telemetry.Init(*cfg.Telemetry) + if c.lazyInit { + if err := lazyInitNodeDir(io, nodeDir); err != nil { + return fmt.Errorf("unable to lazy-init the node directory, %w", err) + } + } - // Write genesis file if missing. - // NOTE: this will be dropped in a PR that resolves issue #1886: - // https://github.com/gnolang/gno/issues/1886 + // Load the configuration + cfg, err := config.LoadConfig(nodeDir) + if err != nil { + return fmt.Errorf("unable to load config, %w", err) + } + + // Check if the genesis.json exists if !osm.FileExists(genesisPath) { - // Create priv validator first. - // Need it to generate genesis.json - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - pk := priv.GetPubKey() - - // Generate genesis.json file - if err := generateGenesisFile(genesisPath, pk, c); err != nil { - return fmt.Errorf("unable to generate genesis file: %w", err) + if !c.lazyInit { + return errMissingGenesis + } + + // Load the private validator secrets + privateKey := privval.LoadFilePV( + cfg.PrivValidatorKeyFile(), + cfg.PrivValidatorStateFile(), + ) + + // Init a new genesis.json + if err := lazyInitGenesis(io, c, genesisPath, privateKey.GetPubKey()); err != nil { + return fmt.Errorf("unable to initialize genesis.json, %w", err) } } - // Initialize the indexer config - txEventStoreCfg, err := getTxEventStoreConfig(c) - if err != nil { - return fmt.Errorf("unable to parse indexer config, %w", err) + // Initialize telemetry + if err := telemetry.Init(*cfg.Telemetry); err != nil { + return fmt.Errorf("unable to initialize telemetry, %w", err) } - cfg.TxEventStore = txEventStoreCfg - // Create application and node. - gnoApp, err := gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) + // Create application and node + cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) if err != nil { - return fmt.Errorf("error in creating new app: %w", err) + return fmt.Errorf("unable to create the Gnoland app, %w", err) } - cfg.LocalApp = gnoApp - if logFormat != log.JSONFormat { + // Print the starting graphic + if c.logFormat != string(log.JSONFormat) { io.Println(startGraphic) } + // Create a default node, with the given setup gnoNode, err := node.DefaultNewNode(cfg, genesisPath, logger) if err != nil { - return fmt.Errorf("error in creating node: %w", err) + return fmt.Errorf("unable to create the Gnoland node, %w", err) + } + + // Start the node (async) + if err := gnoNode.Start(); err != nil { + return fmt.Errorf("unable to start the Gnoland node, %w", err) } - if c.skipStart { - io.ErrPrintln("'--skip-start' is set. Exiting.") + // Set up the wait context + nodeCtx, _ := signal.NotifyContext( + ctx, + os.Interrupt, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + // Wait for the exit signal + <-nodeCtx.Done() + + if !gnoNode.IsRunning() { return nil } - if err := gnoNode.Start(); err != nil { - return fmt.Errorf("error in start node: %w", err) + // Gracefully stop the gno node + if err := gnoNode.Stop(); err != nil { + return fmt.Errorf("unable to gracefully stop the Gnoland node, %w", err) } - osm.TrapSignal(func() { - if gnoNode.IsRunning() { - _ = gnoNode.Stop() + return nil +} + +// lazyInitNodeDir initializes new secrets, and a default configuration +// in the given node directory, if not present +func lazyInitNodeDir(io commands.IO, nodeDir string) error { + var ( + configPath = constructConfigPath(nodeDir) + secretsPath = constructSecretsPath(nodeDir) + ) + + // Check if the configuration already exists + if !osm.FileExists(configPath) { + // Create the gnoland config options + cfg := &configInitCfg{ + configCfg: configCfg{ + configPath: constructConfigPath(nodeDir), + }, } - // Sync the logger before exiting - _ = zapLogger.Sync() - }) + // Run gnoland config init + if err := execConfigInit(cfg, io); err != nil { + return fmt.Errorf("unable to initialize config, %w", err) + } + + io.Printfln("WARN: Initialized default node config at %q", filepath.Dir(cfg.configPath)) + io.Println() + } + + // Create the gnoland secrets options + secrets := &secretsInitCfg{ + commonAllCfg: commonAllCfg{ + dataDir: secretsPath, + }, + forceOverwrite: false, // existing secrets shouldn't be pruned + } - // Run forever - select {} + // Run gnoland secrets init + err := execSecretsInit(secrets, []string{}, io) + if err == nil { + io.Printfln("WARN: Initialized default node secrets at %q", secrets.dataDir) + + return nil + } + + // Check if the error is valid + if errors.Is(err, errOverwriteNotEnabled) { + // No new secrets were generated + return nil + } + + return fmt.Errorf("unable to initialize secrets, %w", err) +} + +// lazyInitGenesis a new genesis.json file, with a signle validator +func lazyInitGenesis( + io commands.IO, + c *startCfg, + genesisPath string, + publicKey crypto.PubKey, +) error { + // Check if the genesis.json is present + if osm.FileExists(genesisPath) { + return nil + } + + // Generate the new genesis.json file + if err := generateGenesisFile(genesisPath, publicKey, c); err != nil { + return fmt.Errorf("unable to generate genesis file, %w", err) + } + + io.Printfln("WARN: Initialized genesis.json at %q", genesisPath) + + return nil +} + +// initializeLogger initializes the zap logger using the given format and log level, +// outputting to the given IO +func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logger, error) { + // Initialize the log level + level, err := zapcore.ParseLevel(logLevel) + if err != nil { + return nil, fmt.Errorf("unable to parse log level, %w", err) + } + + // Initialize the log format + format := log.Format(strings.ToLower(logFormat)) + + // Initialize the zap logger + return log.GetZapLoggerFn(format)(io, level), nil } func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error { @@ -371,27 +429,3 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro return nil } - -// getTxEventStoreConfig constructs an event store config from provided user options -func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { - var cfg *eventstorecfg.Config - - switch c.txEventStoreType { - case file.EventStoreType: - if c.txEventStorePath == "" { - return nil, errors.New("unspecified file transaction indexer path") - } - - // Fill out the configuration - cfg = &eventstorecfg.Config{ - EventStoreType: file.EventStoreType, - Params: map[string]any{ - file.Path: c.txEventStorePath, - }, - } - default: - cfg = eventstorecfg.DefaultEventStoreConfig() - } - - return cfg, nil -} diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index cdec6de0f99..2f620fcd360 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -4,38 +4,110 @@ import ( "bytes" "context" "path/filepath" + "strings" "testing" "time" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" ) -func TestStartInitialize(t *testing.T) { - t.Parallel() +// retryUntilTimeout runs the callback until the timeout is exceeded, or +// the callback returns a flag indicating completion +func retryUntilTimeout(ctx context.Context, cb func() bool) error { + ch := make(chan error, 1) + + go func() { + defer close(ch) + + for { + select { + case <-ctx.Done(): + ch <- ctx.Err() + + return + default: + retry := cb() + if !retry { + ch <- nil + return + } + } + + time.Sleep(500 * time.Millisecond) + } + }() + + return <-ch +} + +// prepareNodeRPC sets the RPC listen address for the node to be an arbitrary +// free address. Setting the listen port to a free port on the machine avoids +// node collisions between different testing suites +func prepareNodeRPC(t *testing.T, nodeDir string) { + t.Helper() + + path := constructConfigPath(nodeDir) + args := []string{ + "config", + "init", + "--config-path", + path, + } + + // Prepare the IO + mockOut := new(bytes.Buffer) + mockErr := new(bytes.Buffer) + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOut)) + io.SetErr(commands.WriteNopCloser(mockErr)) + + // Prepare the cmd context + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - // NOTE: cannot be txtar tests as they use their own parsing for the - // "gnoland" command line. See pkg/integration. + // Run config init + require.NoError(t, newRootCmd(io).ParseAndRun(ctx, args)) + + args = []string{ + "config", + "set", + "--config-path", + path, + "rpc.laddr", + "tcp://0.0.0.0:0", + } + + // Run config set + require.NoError(t, newRootCmd(io).ParseAndRun(ctx, args)) +} + +func TestStart_Lazy(t *testing.T) { + t.Parallel() var ( nodeDir = t.TempDir() genesisFile = filepath.Join(nodeDir, "test_genesis.json") - - args = []string{ - "start", - "--skip-start", - "--skip-failing-genesis-txs", - - // These two flags are tested together as they would otherwise - // pollute this directory (cmd/gnoland) if not set. - "--data-dir", - nodeDir, - "--genesis", - genesisFile, - } ) + // Prepare the config + prepareNodeRPC(t, nodeDir) + + args := []string{ + "start", + "--lazy", + "--skip-failing-genesis-txs", + + // These two flags are tested together as they would otherwise + // pollute this directory (cmd/gnoland) if not set. + "--data-dir", + nodeDir, + "--genesis", + genesisFile, + } + // Prepare the IO mockOut := new(bytes.Buffer) mockErr := new(bytes.Buffer) @@ -44,13 +116,49 @@ func TestStartInitialize(t *testing.T) { io.SetErr(commands.WriteNopCloser(mockErr)) // Create and run the command - ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) defer cancelFn() - cmd := newRootCmd(io) - require.NoError(t, cmd.ParseAndRun(ctx, args)) + // Set up the command ctx + g, gCtx := errgroup.WithContext(ctx) + + // Start the node + g.Go(func() error { + return newRootCmd(io).ParseAndRun(gCtx, args) + }) + + // Set up the retry ctx + retryCtx, retryCtxCancelFn := context.WithTimeout(ctx, 5*time.Second) + defer retryCtxCancelFn() + + // This is a very janky way to verify the node has started. + // The alternative is to poll the node's RPC endpoints, but for some reason + // this introduces a lot of flakyness to the testing suite -- shocking! + // In an effort to keep this simple, and avoid randomly failing tests, + // we query the CLI output of the command + require.NoError(t, retryUntilTimeout(retryCtx, func() bool { + return !strings.Contains(mockOut.String(), startGraphic) + })) + + cancelFn() // stop the node + require.NoError(t, g.Wait()) - // Make sure the directory is created - assert.DirExists(t, nodeDir) + // Make sure the genesis is generated assert.FileExists(t, genesisFile) + + // Make sure the config is generated (default) + assert.FileExists(t, constructConfigPath(nodeDir)) + + // Make sure the secrets are generated + var ( + secretsPath = constructSecretsPath(nodeDir) + validatorKeyPath = filepath.Join(secretsPath, defaultValidatorKeyName) + validatorStatePath = filepath.Join(secretsPath, defaultValidatorStateName) + nodeKeyPath = filepath.Join(secretsPath, defaultNodeKeyName) + ) + + assert.DirExists(t, secretsPath) + assert.FileExists(t, validatorKeyPath) + assert.FileExists(t, validatorStatePath) + assert.FileExists(t, nodeKeyPath) } diff --git a/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar b/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar new file mode 100644 index 00000000000..7275a8cb954 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar @@ -0,0 +1,43 @@ +# load the package from $WORK directory +loadpkg gno.land/r/demo/simple_event $WORK/event + +# start a new node +gnoland start + +## test1 account should be available on default +gnokey query auth/accounts/${USER_ADDR_test1} +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' +stdout ' "coins": "[0-9]*ugnot",' # dynamic +stdout ' "public_key": null,' +stdout ' "account_number": "0",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # empty + + +## sign +gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 0 test1 +stdout 'Tx successfully signed and saved to ' + +## broadcast +gnokey broadcast $WORK/multi/multi_msg.tx -quiet=false + + +-- event/simple_event.gno -- +package simple_event + +import ( + "std" +) + +func Event(value string) { + std.Emit("TAG", "KEY", value) +} + +-- multi/multi_msg.tx -- +{"msg":[{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""} + diff --git a/gno.land/cmd/gnoland/testdata/issue_1786.txtar b/gno.land/cmd/gnoland/testdata/issue_1786.txtar index 44ea17674c9..7c92e81dfb6 100644 --- a/gno.land/cmd/gnoland/testdata/issue_1786.txtar +++ b/gno.land/cmd/gnoland/testdata/issue_1786.txtar @@ -5,7 +5,7 @@ loadpkg gno.land/r/demo/wugnot gnoland start # add contract -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 6000000 -broadcast -chainid=tendermint_test test1 stdout OK! # approve wugnot to `proxywugnot ≈ g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3` diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar new file mode 100644 index 00000000000..ac7988616a4 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -0,0 +1,183 @@ +# This tests ensure the consistency of the std.PrevRealm function, in the +# following situations: +# +# +# | Num | Msg Type | Call from | Entry Point | Result | +# |-----|:--------:|:-------------------:|:---------------:|:------------:| +# | 1 | MsgCall | wallet direct | myrlm.A() | user address | +# | 2 | | | myrlm.B() | user address | +# | 3 | | through /r/foo | myrlm.A() | r/foo | +# | 4 | | | myrlm.B() | r/foo | +# | 5 | | through /p/demo/bar | myrlm.A() | user address | +# | 6 | | | myrlm.B() | user address | +# | 7 | MsgRun | wallet direct | myrlm.A() | user address | +# | 8 | | | myrlm.B() | user address | +# | 9 | | through /r/foo | myrlm.A() | r/foo | +# | 10 | | | myrlm.B() | r/foo | +# | 11 | | through /p/demo/bar | myrlm.A() | user address | +# | 12 | | | myrlm.B() | user address | +# | 13 | MsgCall | wallet direct | std.PrevRealm() | user address | +# | 14 | MsgRun | wallet direct | std.PrevRealm() | user address | + +# Init +## deploy myrlm +loadpkg gno.land/r/myrlm $WORK/r/myrlm +## deploy r/foo +loadpkg gno.land/r/foo $WORK/r/foo +## deploy p/demo/bar +loadpkg gno.land/p/demo/bar $WORK/p/demo/bar + +## start a new node +gnoland start + +env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 + +# Test cases +## 1. MsgCall -> myrlm.A: user address +gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${USER_ADDR_test1} + +## 2. MsgCall -> myrealm.B -> myrlm.A: user address +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${USER_ADDR_test1} + +## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo +gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${RFOO_ADDR} + +## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${RFOO_ADDR} + +## 5. MsgCall -> p/demo/bar.A -> myrlm.A: user address +gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${USER_ADDR_test1} + +## 6. MsgCall -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address +gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${USER_ADDR_test1} + +## 7. MsgRun -> myrlm.A: user address +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +stdout ${USER_ADDR_test1} + +## 8. MsgRun -> myrealm.B -> myrlm.A: user address +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +stdout ${USER_ADDR_test1} + +## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +stdout ${RFOO_ADDR} + +## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +stdout ${RFOO_ADDR} + +## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +stdout ${USER_ADDR_test1} + +## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +stdout ${USER_ADDR_test1} + +## 13. MsgCall -> std.PrevRealm(): user address +gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +stdout ${USER_ADDR_test1} + +## 14. MsgRun -> std.PrevRealm(): user address +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +stdout ${USER_ADDR_test1} + +-- r/myrlm/myrlm.gno -- +package myrlm + +import "std" + +func A() string { + return std.PrevRealm().Addr().String() +} + +func B() string { + return A() +} +-- r/foo/foo.gno -- +package foo + +import "gno.land/r/myrlm" + +func A() string { + return myrlm.A() +} + +func B() string { + return myrlm.B() +} +-- p/demo/bar/bar.gno -- +package bar + +import "gno.land/r/myrlm" + +func A() string { + return myrlm.A() +} + +func B() string { + return myrlm.B() +} +-- run/myrlmA.gno -- +package main + +import myrlm "gno.land/r/myrlm" + +func main() { + println(myrlm.A()) +} +-- run/myrlmB.gno -- +package main + +import "gno.land/r/myrlm" + +func main() { + println(myrlm.B()) +} +-- run/fooA.gno -- +package main + +import "gno.land/r/foo" + +func main() { + println(foo.A()) +} +-- run/fooB.gno -- +package main + +import "gno.land/r/foo" + +func main() { + println(foo.B()) +} +-- run/barA.gno -- +package main + +import "gno.land/p/demo/bar" + +func main() { + println(bar.A()) +} +-- run/barB.gno -- +package main + +import "gno.land/p/demo/bar" + +func main() { + println(bar.B()) +} +-- run/baz.gno -- +package main + +import "std" + +func main() { + println(std.PrevRealm().Addr().String()) +} diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar index abc3615d79c..71ef6400471 100644 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar @@ -7,10 +7,10 @@ adduser test2 gnoland start ## add realm_banker -gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 ## add realm_banker with long package_name -gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 ## test2 spend all balance gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index 3baf9d3231b..61b15e932ca 100644 --- a/gno.land/genesis/genesis_txs.jsonl +++ b/gno.land/genesis/genesis_txs.jsonl @@ -12,6 +12,6 @@ {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n - [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n - [gno.land/r/demo/nft/nft.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in Gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/banktest:](/r/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/banktest](/r/banktest) and the [quickstart guide](/r/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards\nGetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\nBOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\ngnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards\nGetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\nBOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\ngnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} \ No newline at end of file +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index bd8079517d8..9bcba7997ae 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -205,12 +205,12 @@ func TestSendMultiple_Integration(t *testing.T) { // Execute send res, err := client.Send(baseCfg, msg1, msg2) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "", string(res.DeliverTx.Data)) // Get the new account balance account, _, err := client.QueryAccount(toAddress) - assert.Nil(t, err) + assert.NoError(t, err) expected := std.Coins{{"ugnot", int64(amount1 + amount2)}} got := account.GetCoins() diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index cd0c946f907..7669d5cce95 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -157,31 +157,34 @@ func PanicOnFailingTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { // InitChainer returns a function that can initialize the chain with genesis. func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank.BankKeeperI, resHandler GenesisTxHandler) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - // Get genesis state. - genState := req.AppState.(GnoGenesisState) - // Parse and set genesis state balances. - for _, bal := range genState.Balances { - acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) - acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) - if err != nil { - panic(err) + if req.AppState != nil { + // Get genesis state + genState := req.AppState.(GnoGenesisState) + + // Parse and set genesis state balances + for _, bal := range genState.Balances { + acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) + acctKpr.SetAccount(ctx, acc) + err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) + if err != nil { + panic(err) + } } - } - // Run genesis txs. - for _, tx := range genState.Txs { - res := baseApp.Deliver(tx) - if res.IsErr() { - ctx.Logger().Error( - "Unable to deliver genesis tx", - "log", res.Log, - "error", res.Error, - "gas-used", res.GasUsed, - ) + // Run genesis txs + for _, tx := range genState.Txs { + res := baseApp.Deliver(tx) + if res.IsErr() { + ctx.Logger().Error( + "Unable to deliver genesis tx", + "log", res.Log, + "error", res.Error, + "gas-used", res.GasUsed, + ) + } + + resHandler(ctx, tx, res) } - - resHandler(ctx, tx, res) } // Done! diff --git a/gno.land/pkg/integration/testdata/improved-coins.txtar b/gno.land/pkg/integration/testdata/improved-coins.txtar new file mode 100644 index 00000000000..8950371afb3 --- /dev/null +++ b/gno.land/pkg/integration/testdata/improved-coins.txtar @@ -0,0 +1,86 @@ +loadpkg gno.land/r/demo/coins $WORK + +gnoland start + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "MakeNewCoins" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(300 int64)' +stdout '(321 int64)' +stdout '("ugnot" string)' +stdout '("example" string)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "AddCoin" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(300 int64)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "SubCoin" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(123 int64)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "StringZeroCoin" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '("0ugnot" string)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "IsZero" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(true bool)' +stdout '(false bool)' +stdout '(false bool)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "IsPositive" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(false bool)' +stdout '(false bool)' +stdout '(true bool)' + +gnokey maketx call -pkgpath gno.land/r/demo/coins -func "IsNegative" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(true bool)' +stdout '(false bool)' +stdout '(false bool)' + +-- coins.gno -- +package coins + +import "std" + +func MakeNewCoins() std.Coins { + coin1 := std.NewCoin("ugnot", 123) + coin2 := std.NewCoin("example", 321) + coin3 := std.NewCoin("ugnot", 177) + + + return std.NewCoins(coin1, coin2, coin3) +} + +func AddCoin() std.Coin { + coin1 := std.NewCoin("ugnot", 123) + coin2 := std.NewCoin("ugnot", 177) + return coin1.Add(coin2) +} + +func SubCoin() std.Coin { + coin1 := std.NewCoin("ugnot", 300) + coin2 := std.NewCoin("ugnot", 177) + return coin1.Sub(coin2) +} + +func StringZeroCoin() string { + coin1 := std.NewCoin("ugnot", 0) + return coin1.String() +} + +func IsZero() (bool, bool, bool) { + coin1 := std.NewCoin("ugnot", 0) + coin2 := std.NewCoin("ugnot", 123) + coin3 := std.NewCoin("ugnot", -123) + return coin1.IsZero(), coin2.IsZero(), coin3.IsZero() +} + +func IsPositive() (bool, bool, bool) { + coin1 := std.NewCoin("ugnot", -123) + coin2 := std.NewCoin("ugnot", 0) + coin3 := std.NewCoin("ugnot", 123) + return coin1.IsPositive(), coin2.IsPositive(), coin3.IsPositive() +} + +func IsNegative() (bool, bool, bool) { + coin1 := std.NewCoin("ugnot", -123) + coin2 := std.NewCoin("ugnot", 0) + coin3 := std.NewCoin("ugnot", 123) + return coin1.IsNegative(), coin2.IsNegative(), coin3.IsNegative() +} + diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index e1dd31846e7..ae77021aa06 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -1,12 +1,17 @@ package vm import ( + "context" "fmt" "strings" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/telemetry" + "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" ) type vmHandler struct { @@ -51,14 +56,6 @@ func (vh vmHandler) handleMsgCall(ctx sdk.Context, msg MsgCall) (res sdk.Result) } res.Data = []byte(resstr) return - /* TODO handle events. - ctx.EventManager().EmitEvent( - sdk.NewEvent( - sdk.EventTypeMessage, - sdk.NewAttribute(sdk.AttributeKeyXXX, types.AttributeValueXXX), - ), - ) - */ } // Handle MsgRun. @@ -71,7 +68,7 @@ func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { return } -//---------------------------------------- +// ---------------------------------------- // Query // query paths @@ -84,27 +81,58 @@ const ( QueryFile = "qfile" ) -func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - switch secondPart(req.Path) { +func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery { + var ( + res abci.ResponseQuery + path = secondPart(req.Path) + ) + + switch path { case QueryPackage: - return vh.queryPackage(ctx, req) + res = vh.queryPackage(ctx, req) case QueryStore: - return vh.queryStore(ctx, req) + res = vh.queryStore(ctx, req) case QueryRender: - return vh.queryRender(ctx, req) + res = vh.queryRender(ctx, req) case QueryFuncs: - return vh.queryFuncs(ctx, req) + res = vh.queryFuncs(ctx, req) case QueryEval: - return vh.queryEval(ctx, req) + res = vh.queryEval(ctx, req) case QueryFile: - return vh.queryFile(ctx, req) + res = vh.queryFile(ctx, req) default: - res = sdk.ABCIResponseQueryFromError( + return sdk.ABCIResponseQueryFromError( std.ErrUnknownRequest(fmt.Sprintf( "unknown vm query endpoint %s in %s", secondPart(req.Path), req.Path))) + } + + // Log the telemetry + logQueryTelemetry(path, res.IsErr()) + + return res +} + +// logQueryTelemetry logs the relevant VM query telemetry +func logQueryTelemetry(path string, isErr bool) { + if !telemetry.MetricsEnabled() { return } + + metrics.VMQueryCalls.Add( + context.Background(), + 1, + metric.WithAttributes( + attribute.KeyValue{ + Key: "path", + Value: attribute.StringValue(path), + }, + ), + ) + + if isErr { + metrics.VMQueryErrors.Add(context.Background(), 1) + } } // queryPackage fetch a package's files. @@ -187,7 +215,7 @@ func (vh vmHandler) queryFile(ctx sdk.Context, req abci.RequestQuery) (res abci. return } -//---------------------------------------- +// ---------------------------------------- // misc func abciResult(err error) sdk.Result { diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index ef260bd3c42..a77ddac3e28 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -4,9 +4,9 @@ package vm import ( "bytes" + "context" "fmt" "os" - "regexp" "strings" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" @@ -17,6 +17,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/telemetry" + "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" ) const ( @@ -131,8 +135,6 @@ func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { } } -var reRunPath = regexp.MustCompile(`gno\.land/r/g[a-z0-9]+/run`) - // AddPackage adds a package with given fileset. func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { creator := msg.Creator @@ -156,7 +158,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { return ErrInvalidPkgPath("package already exists: " + pkgPath) } - if reRunPath.MatchString(pkgPath) { + if gno.ReGnoRunPath.MatchString(pkgPath) { return ErrInvalidPkgPath("reserved package name: " + pkgPath) } @@ -218,6 +220,17 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { } }() m2.RunMemPackage(memPkg, true) + + // Log the telemetry + logTelemetry( + m2.GasMeter.GasConsumed(), + m2.Cycles, + attribute.KeyValue{ + Key: "operation", + Value: attribute.StringValue("m_addpkg"), + }, + ) + return nil } @@ -315,7 +328,19 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { res += "\n" } } + + // Log the telemetry + logTelemetry( + m.GasMeter.GasConsumed(), + m.Cycles, + attribute.KeyValue{ + Key: "operation", + Value: attribute.StringValue("m_call"), + }, + ) + res += "\n\n" // use `\n\n` as separator to separate results for single tx with multi msgs + return res, nil // TODO pay for gas? TODO see context? } @@ -421,6 +446,17 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { }() m2.RunMain() res = buf.String() + + // Log the telemetry + logTelemetry( + m2.GasMeter.GasConsumed(), + m2.Cycles, + attribute.KeyValue{ + Key: "operation", + Value: attribute.StringValue("m_run"), + }, + ) + return res, nil } @@ -639,3 +675,35 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err return res, nil } } + +// logTelemetry logs the VM processing telemetry +func logTelemetry( + gasUsed int64, + cpuCycles int64, + attributes ...attribute.KeyValue, +) { + if !telemetry.MetricsEnabled() { + return + } + + // Record the operation frequency + metrics.VMExecMsgFrequency.Add( + context.Background(), + 1, + metric.WithAttributes(attributes...), + ) + + // Record the CPU cycles + metrics.VMCPUCycles.Record( + context.Background(), + cpuCycles, + metric.WithAttributes(attributes...), + ) + + // Record the gas used + metrics.VMGasUsed.Record( + context.Background(), + gasUsed, + metric.WithAttributes(attributes...), + ) +} diff --git a/gnovm/cmd/gno/bug.go b/gnovm/cmd/gno/bug.go index 98e6fb07319..8d4661e3de8 100644 --- a/gnovm/cmd/gno/bug.go +++ b/gnovm/cmd/gno/bug.go @@ -61,6 +61,17 @@ func newBugCmd(io commands.IO) *commands.Command { Name: "bug", ShortUsage: "bug", ShortHelp: "Start a bug report", + LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. + +The new issue body is prefilled for you with the following information: + +- Go version (example: go1.22.2) +- OS and CPU architecture (example: linux/amd64) +- Gno commit hash causing the issue (example: f24690e7ebf325bffcfaf9e328c3df8e6b21e50c) + +The rest of the report consists of markdown sections such as ### Steps to reproduce +that you can edit. +`, }, cfg, func(_ context.Context, args []string) error { @@ -74,7 +85,7 @@ func (c *bugCfg) RegisterFlags(fs *flag.FlagSet) { &c.skipBrowser, "skip-browser", false, - "do not open the browser", + "output a prefilled issue template on the cli instead", ) } diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index e2a7e53fb69..c1974094da0 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -5,6 +5,8 @@ import ( "errors" "flag" "fmt" + "go/scanner" + "io" "os" "path/filepath" "regexp" @@ -68,10 +70,6 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { } hasError := false - addIssue := func(issue lintIssue) { - hasError = true - fmt.Fprint(io.Err(), issue.String()+"\n") - } for _, pkgPath := range pkgPaths { if verbose { @@ -81,16 +79,18 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { // Check if 'gno.mod' exists gnoModPath := filepath.Join(pkgPath, "gno.mod") if !osm.FileExists(gnoModPath) { - addIssue(lintIssue{ + hasError = true + issue := lintIssue{ Code: lintNoGnoMod, Confidence: 1, Location: pkgPath, Msg: "missing 'gno.mod' file", - }) + } + fmt.Fprint(io.Err(), issue.String()+"\n") } // Handle runtime errors - catchRuntimeError(pkgPath, addIssue, func() { + hasError = catchRuntimeError(pkgPath, io.Err(), func() { stdout, stdin, stderr := io.Out(), io.In(), io.Err() testStore := tests.TestStore( rootDir, "", @@ -130,7 +130,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { } tm.RunFiles(testfiles.Files...) - }) + }) || hasError // TODO: Add more checkers } @@ -164,47 +164,33 @@ func guessSourcePath(pkg, source string) string { // XXX: Ideally, error handling should encapsulate location details within a dedicated error type. var reParseRecover = regexp.MustCompile(`^([^:]+):(\d+)(?::\d+)?:? *(.*)$`) -func catchRuntimeError(pkgPath string, addIssue func(issue lintIssue), action func()) { +func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (hasError bool) { defer func() { // Errors catched here mostly come from: gnovm/pkg/gnolang/preprocess.go r := recover() if r == nil { return } - - var err error + hasError = true switch verr := r.(type) { case *gno.PreprocessError: - err = verr.Unwrap() + err := verr.Unwrap() + fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + case scanner.ErrorList: + for _, err := range verr { + fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + } case error: - err = verr + fmt.Fprint(stderr, issueFromError(pkgPath, verr).String()+"\n") case string: - err = errors.New(verr) + fmt.Fprint(stderr, issueFromError(pkgPath, errors.New(verr)).String()+"\n") default: panic(r) } - - var issue lintIssue - issue.Confidence = 1 - issue.Code = lintGnoError - - parsedError := strings.TrimSpace(err.Error()) - parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") - - matches := reParseRecover.FindStringSubmatch(parsedError) - if len(matches) == 4 { - sourcepath := guessSourcePath(pkgPath, matches[1]) - issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2]) - issue.Msg = strings.TrimSpace(matches[3]) - } else { - issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath)) - issue.Msg = err.Error() - } - - addIssue(issue) }() action() + return } type lintCode int @@ -229,3 +215,23 @@ func (i lintIssue) String() string { // TODO: consider crafting a doc URL based on Code. return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code) } + +func issueFromError(pkgPath string, err error) lintIssue { + var issue lintIssue + issue.Confidence = 1 + issue.Code = lintGnoError + + parsedError := strings.TrimSpace(err.Error()) + parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") + + matches := reParseRecover.FindStringSubmatch(parsedError) + if len(matches) == 4 { + sourcepath := guessSourcePath(pkgPath, matches[1]) + issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2]) + issue.Msg = strings.TrimSpace(matches[3]) + } else { + issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath)) + issue.Msg = err.Error() + } + return issue +} diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 5773ae0a06f..fff05726e53 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -16,6 +16,9 @@ func TestLintApp(t *testing.T) { }, { args: []string{"lint", "--set-exit-status=0", "../../tests/integ/package_not_declared/main.gno"}, stderrShouldContain: "main.gno:4: name fmt not declared (code=2).", + }, { + args: []string{"lint", "--set-exit-status=0", "../../tests/integ/several-lint-errors/main.gno"}, + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6", }, { args: []string{"lint", "--set-exit-status=0", "../../tests/integ/run_main/"}, stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 6c310cd7425..19d7b121970 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -128,7 +128,7 @@ func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.remote, "remote", - "test3.gno.land:36657", + "test3.gno.land:26657", "remote for fetching gno modules", ) diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 0c5218613a9..65315909f72 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "io" "os" "path/filepath" "strings" @@ -102,7 +103,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { } // read files - files, err := parseFiles(args) + files, err := parseFiles(args, stderr) if err != nil { return err } @@ -135,15 +136,16 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { return nil } -func parseFiles(fnames []string) ([]*gno.FileNode, error) { +func parseFiles(fnames []string, stderr io.WriteCloser) ([]*gno.FileNode, error) { files := make([]*gno.FileNode, 0, len(fnames)) + var hasError bool for _, fname := range fnames { if s, err := os.Stat(fname); err == nil && s.IsDir() { subFns, err := listNonTestFiles(fname) if err != nil { return nil, err } - subFiles, err := parseFiles(subFns) + subFiles, err := parseFiles(subFns, stderr) if err != nil { return nil, err } @@ -154,7 +156,14 @@ func parseFiles(fnames []string) ([]*gno.FileNode, error) { // in either case not a file we can parse. return nil, err } - files = append(files, gno.MustReadFile(fname)) + + hasError = catchRuntimeError(fname, stderr, func() { + files = append(files, gno.MustReadFile(fname)) + }) + } + + if hasError { + os.Exit(1) } return files, nil } diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 8b48e10d422..e09e06c0418 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -325,7 +325,15 @@ func gnoTestPkg( memPkg := gno.ReadMemPackage(pkgPath, gnoPkgPath) // tfiles, ifiles := gno.ParseMemPackageTests(memPkg) - tfiles, ifiles := parseMemPackageTests(memPkg) + var tfiles, ifiles *gno.FileSet + + hasError := catchRuntimeError(gnoPkgPath, stderr, func() { + tfiles, ifiles = parseMemPackageTests(memPkg) + }) + + if hasError { + os.Exit(1) + } testPkgName := getPkgNameFromFileset(ifiles) // run test files in pkg @@ -639,7 +647,7 @@ func parseMemPackageTests(memPkg *std.MemPackage) (tset, itset *gno.FileSet) { } n, err := gno.ParseFile(mfile.Name, mfile.Body) if err != nil { - panic(errors.Wrap(err, "parsing file "+mfile.Name)) + panic(err) } if n == nil { panic("should not happen") diff --git a/gnovm/cmd/gno/transpile.go b/gnovm/cmd/gno/transpile.go index 3469304bea2..7d01c603555 100644 --- a/gnovm/cmd/gno/transpile.go +++ b/gnovm/cmd/gno/transpile.go @@ -250,7 +250,9 @@ func transpileFile(srcPath string, opts *transpileOptions) error { if !flags.skipImports { importPaths := getPathsFromImportSpec(transpileRes.Imports) for _, path := range importPaths { - transpilePkg(path, opts) + if err := transpilePkg(path, opts); err != nil { + return err + } } } diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 8f2c5054a8a..6f1584dcc28 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1179,7 +1179,7 @@ func ParseMemPackage(memPkg *std.MemPackage) (fset *FileSet) { } n, err := ParseFile(mfile.Name, mfile.Body) if err != nil { - panic(errors.Wrap(err, "parsing file "+mfile.Name)) + panic(err) } if memPkg.Name != string(n.PkgName) { panic(fmt.Sprintf( diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ddc72c3a048..4331e2aed9e 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -884,17 +884,22 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { // Left not const, Right not const ------------------ if n.Op == EQL || n.Op == NEQ { // If == or !=, no conversions. - } else if lnt, ok := lt.(*NativeType); ok { + } else if lnt, ok := lt.(*NativeType); ok && isNative(rt) { if debug { if !isShift { assertSameTypes(lt, rt) } } - // If left and right are native type, + // If left and right are native type, and same type // convert left and right to gno, then // convert result back to native. // // get concrete native base type. + if lt.TypeID() != rt.TypeID() { + panic(fmt.Sprintf( + "incompatible types in binary expression: %v %v %v", + lt.TypeID(), n.Op, rt.TypeID())) + } pt := go2GnoBaseType(lnt.Type).(PrimitiveType) // convert n.Left to (gno) pt type, ln := Expr(Call(pt.String(), n.Left)) @@ -932,7 +937,7 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { if lt.TypeID() != rt.TypeID() { panic(fmt.Sprintf( "incompatible types in binary expression: %v %v %v", - n.Left, n.Op, n.Right)) + lt.TypeID(), n.Op, rt.TypeID())) } } else { checkOrConvertType(store, last, &n.Left, rt, false) @@ -945,7 +950,7 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { if lt.TypeID() != rt.TypeID() { panic(fmt.Sprintf( "incompatible types in binary expression: %v %v %v", - n.Left, n.Op, n.Right)) + lt.TypeID(), n.Op, rt.TypeID())) } } } diff --git a/gnovm/pkg/gnolang/preprocess_test.go b/gnovm/pkg/gnolang/preprocess_test.go new file mode 100644 index 00000000000..2419a385e14 --- /dev/null +++ b/gnovm/pkg/gnolang/preprocess_test.go @@ -0,0 +1,60 @@ +package gnolang + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPreprocess_BinaryExpressionOneNative(t *testing.T) { + pn := NewPackageNode("time", "time", nil) + pn.DefineGoNativeValue("Millisecond", time.Millisecond) + pn.DefineGoNativeValue("Second", time.Second) + pn.DefineGoNativeType(reflect.TypeOf(time.Duration(0))) + pv := pn.NewPackage() + store := gonativeTestStore(pn, pv) + store.SetBlockNode(pn) + + const src = `package main + import "time" +func main() { + var a int64 = 2 + println(time.Second * a) + +}` + n := MustParseFile("main.go", src) + + defer func() { + err := recover() + assert.Contains(t, fmt.Sprint(err), "incompatible types in binary expression") + }() + Preprocess(store, pn, n) +} + +func TestPreprocess_BinaryExpressionBothNative(t *testing.T) { + pn := NewPackageNode("time", "time", nil) + pn.DefineGoNativeValue("March", time.March) + pn.DefineGoNativeValue("Wednesday", time.Wednesday) + pn.DefineGoNativeType(reflect.TypeOf(time.Month(0))) + pn.DefineGoNativeType(reflect.TypeOf(time.Weekday(0))) + pv := pn.NewPackage() + store := gonativeTestStore(pn, pv) + store.SetBlockNode(pn) + + const src = `package main + import "time" +func main() { + println(time.March * time.Wednesday) + +}` + n := MustParseFile("main.go", src) + + defer func() { + err := recover() + assert.Contains(t, fmt.Sprint(err), "incompatible types in binary expression") + }() + Preprocess(store, pn, n) +} diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 3f615772426..17b655e6352 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reflect" + "regexp" "strings" ) @@ -1517,10 +1518,14 @@ func isUnsaved(oo Object) bool { // be realms and as such to have their state persisted. This is used by [IsRealmPath]. const realmPathPrefix = "gno.land/r/" +var ReGnoRunPath = regexp.MustCompile(`gno\.land/r/g[a-z0-9]+/run`) + // IsRealmPath determines whether the given pkgpath is for a realm, and as such // should persist the global state. func IsRealmPath(pkgPath string) bool { - return strings.HasPrefix(pkgPath, realmPathPrefix) + return strings.HasPrefix(pkgPath, realmPathPrefix) && + // MsgRun pkgPath aren't realms + !ReGnoRunPath.MatchString(pkgPath) } func prettyJSON(jstr []byte) []byte { diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 948730c4697..fda0a06f3d2 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -817,6 +817,7 @@ type PackageValue struct { fBlocksMap map[Name]*Block } +// IsRealm returns true if pv represents a realm. func (pv *PackageValue) IsRealm() bool { return IsRealmPath(pv.PkgPath) } diff --git a/gnovm/pkg/gnomod/file_test.go b/gnovm/pkg/gnomod/file_test.go index 0012960eb4f..7abfe16f340 100644 --- a/gnovm/pkg/gnomod/file_test.go +++ b/gnovm/pkg/gnomod/file_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/mod/module" ) -const testRemote string = "test3.gno.land:36657" +const testRemote string = "test3.gno.land:26657" func TestFetchDeps(t *testing.T) { for _, tc := range []struct { diff --git a/gnovm/pkg/transpiler/transpiler.go b/gnovm/pkg/transpiler/transpiler.go index cc49aac4c78..e3d817700d0 100644 --- a/gnovm/pkg/transpiler/transpiler.go +++ b/gnovm/pkg/transpiler/transpiler.go @@ -231,7 +231,7 @@ func TranspileBuildPackage(fileOrPkg, goBinary string) error { return err } -var errorRe = regexp.MustCompile(`(?m)^(\S+):(\d+):(\d+): (.+)$`) +var reGoBuildError = regexp.MustCompile(`(?m)^(\S+):(\d+):(\d+): (.+)$`) // parseGoBuildErrors returns a scanner.ErrorList filled with all errors found // in out, which is supposed to be the output of the `go build` command. @@ -240,7 +240,7 @@ var errorRe = regexp.MustCompile(`(?m)^(\S+):(\d+):(\d+): (.+)$`) // See https://github.com/golang/go/issues/62067 func parseGoBuildErrors(out string) error { var errList goscanner.ErrorList - matches := errorRe.FindAllStringSubmatch(out, -1) + matches := reGoBuildError.FindAllStringSubmatch(out, -1) for _, match := range matches { filename := match[1] line, err := strconv.Atoi(match[2]) diff --git a/gnovm/stdlibs/native.go b/gnovm/stdlibs/native.go index 3b1ab719e72..7319e393c35 100644 --- a/gnovm/stdlibs/native.go +++ b/gnovm/stdlibs/native.go @@ -425,25 +425,6 @@ var nativeFuncs = [...]nativeFunc{ )) }, }, - { - "std", - "CurrentRealmPath", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("string")}, - }, - func(m *gno.Machine) { - r0 := libs_std.CurrentRealmPath( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "GetChainID", diff --git a/gnovm/stdlibs/std/coins.gno b/gnovm/stdlibs/std/coins.gno index dd138d30822..47e88e238d2 100644 --- a/gnovm/stdlibs/std/coins.gno +++ b/gnovm/stdlibs/std/coins.gno @@ -1,33 +1,141 @@ package std -import "strconv" +import ( + "math/overflow" + "strconv" +) -// NOTE: this is selectly copied over from pkgs/std/coin.go -// TODO: import all functionality(?). -// TODO: implement Coin/Coins constructors. +// NOTE: this is selectively copied over from tm2/pkgs/std/coin.go -// Coin hold some amount of one currency. +// Coin holds some amount of one currency. // A negative amount is invalid. type Coin struct { Denom string `json:"denom"` Amount int64 `json:"amount"` } +// NewCoin returns a new coin with a denomination and amount +func NewCoin(denom string, amount int64) Coin { + return Coin{ + Denom: denom, + Amount: amount, + } +} + +// String provides a human-readable representation of a coin func (c Coin) String() string { return strconv.Itoa(int(c.Amount)) + c.Denom } +// IsGTE returns true if they are the same type and the receiver is +// an equal or greater value func (c Coin) IsGTE(other Coin) bool { - if c.Denom != other.Denom { - panic("invalid coin denominations: " + c.Denom) - } + mustMatchDenominations(c.Denom, other.Denom) + return c.Amount >= other.Amount } +// IsLT returns true if they are the same type and the receiver is +// a smaller value +func (c Coin) IsLT(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount < other.Amount +} + +// IsEqual returns true if the two sets of Coins have the same value +func (c Coin) IsEqual(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount == other.Amount +} + +// Add adds amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Add(other Coin) Coin { + mustMatchDenominations(c.Denom, other.Denom) + + sum, ok := overflow.Add64(c.Amount, other.Amount) + if !ok { + panic("coin add overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount))) + } + + c.Amount = sum + return c +} + +// Sub subtracts amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Sub(other Coin) Coin { + mustMatchDenominations(c.Denom, other.Denom) + + dff, ok := overflow.Sub64(c.Amount, other.Amount) + if !ok { + panic("coin sub overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount))) + } + c.Amount = dff + + return c +} + +// IsPositive returns true if coin amount is positive. +func (c Coin) IsPositive() bool { + return c.Amount > 0 +} + +// IsNegative returns true if the coin amount is negative and false otherwise. +func (c Coin) IsNegative() bool { + return c.Amount < 0 +} + +// IsZero returns true if the amount of given coin is zero +func (c Coin) IsZero() bool { + return c.Amount == 0 +} + +func mustMatchDenominations(denomA, denomB string) { + if denomA != denomB { + panic("incompatible coin denominations: " + denomA + ", " + denomB) + } +} + // Coins is a set of Coin, one per currency type Coins []Coin +// NewCoins returns a new set of Coins given one or more Coins +// Consolidates any denom duplicates into one, keeping the properties of a mathematical set +func NewCoins(coins ...Coin) Coins { + coinMap := make(map[string]int64) + + for _, coin := range coins { + if currentAmount, exists := coinMap[coin.Denom]; exists { + var ok bool + if coinMap[coin.Denom], ok = overflow.Add64(currentAmount, coin.Amount); !ok { + panic("coin sub overflow/underflow: " + strconv.Itoa(int(currentAmount)) + " +/- " + strconv.Itoa(int(coin.Amount))) + } + } else { + coinMap[coin.Denom] = coin.Amount + } + } + + var setCoins Coins + for denom, amount := range coinMap { + setCoins = append(setCoins, NewCoin(denom, amount)) + } + + return setCoins +} + +// String returns the string representation of Coins func (cz Coins) String() string { + if len(cz) == 0 { + return "" + } + res := "" for i, c := range cz { if i > 0 { @@ -35,35 +143,41 @@ func (cz Coins) String() string { } res += c.String() } + return res } +// AmountOf returns the amount of a specific coin from the Coins set func (cz Coins) AmountOf(denom string) int64 { for _, c := range cz { if c.Denom == denom { return c.Amount } } + return 0 } -func (a Coins) Add(b Coins) Coins { +// Add adds a Coin to the Coins set +func (cz Coins) Add(b Coins) Coins { c := Coins{} - for _, ac := range a { + for _, ac := range cz { bc := b.AmountOf(ac.Denom) ac.Amount += bc c = append(c, ac) } + for _, bc := range b { cc := c.AmountOf(bc.Denom) if cc == 0 { c = append(c, bc) } } + return c } -// expand for usage within natively bound functions. +// expandNative expands for usage within natively bound functions. func (cz Coins) expandNative() (denoms []string, amounts []int64) { denoms = make([]string, len(cz)) amounts = make([]int64, len(cz)) @@ -71,5 +185,6 @@ func (cz Coins) expandNative() (denoms []string, amounts []int64) { denoms[i] = coin.Denom amounts[i] = coin.Amount } + return denoms, amounts } diff --git a/gnovm/stdlibs/std/emit_event.go b/gnovm/stdlibs/std/emit_event.go index 46fea79d43c..8e61f67d58a 100644 --- a/gnovm/stdlibs/std/emit_event.go +++ b/gnovm/stdlibs/std/emit_event.go @@ -17,7 +17,7 @@ func X_emit(m *gno.Machine, typ string, attrs []string) { m.Panic(typedString(err.Error())) } - pkgPath := CurrentRealmPath(m) + _, pkgPath := currentRealm(m) fnIdent := getPrevFunctionNameFromTarget(m, "Emit") evt := gnoEvent{ diff --git a/gnovm/stdlibs/std/emit_event_test.go b/gnovm/stdlibs/std/emit_event_test.go index 10bd8ecacd9..147ad75dbb5 100644 --- a/gnovm/stdlibs/std/emit_event_test.go +++ b/gnovm/stdlibs/std/emit_event_test.go @@ -13,7 +13,10 @@ import ( func TestEmit(t *testing.T) { m := gno.NewMachine("emit", nil) - pkgPath := CurrentRealmPath(m) + + m.Context = ExecContext{} + + _, pkgPath := X_getRealm(m, 0) tests := []struct { name string eventType string diff --git a/gnovm/stdlibs/std/native.gno b/gnovm/stdlibs/std/native.gno index 8043df49882..ef2601eeca3 100644 --- a/gnovm/stdlibs/std/native.gno +++ b/gnovm/stdlibs/std/native.gno @@ -1,10 +1,9 @@ package std -func AssertOriginCall() // injected -func IsOriginCall() bool // injected -func CurrentRealmPath() string // injected -func GetChainID() string // injected -func GetHeight() int64 // injected +func AssertOriginCall() // injected +func IsOriginCall() bool // injected +func GetChainID() string // injected +func GetHeight() int64 // injected func GetOrigSend() Coins { den, amt := origSend() diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index deb0f1268d2..7148a4ba4f4 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -17,13 +17,6 @@ func IsOriginCall(m *gno.Machine) bool { return len(m.Frames) == 2 } -func CurrentRealmPath(m *gno.Machine) string { - if m.Realm != nil { - return m.Realm.Path - } - return "" -} - func GetChainID(m *gno.Machine) string { return m.Context.(ExecContext).ChainID } @@ -98,7 +91,7 @@ func X_callerAt(m *gno.Machine, n int) string { return string(m.MustLastCallFrame(n).LastPackage.GetPkgAddr().Bech32()) } -func X_getRealm(m *gno.Machine, height int) (address string, pkgPath string) { +func X_getRealm(m *gno.Machine, height int) (address, pkgPath string) { var ( ctx = m.Context.(ExecContext) currentCaller crypto.Bech32Address @@ -130,6 +123,12 @@ func X_getRealm(m *gno.Machine, height int) (address string, pkgPath string) { return string(ctx.OrigCaller), "" } +// currentRealm retrieves the current realm's address and pkgPath. +// It's not a native binding; but is used within this package to clarify usage. +func currentRealm(m *gno.Machine) (address, pkgPath string) { + return X_getRealm(m, 0) +} + func X_derivePkgAddr(pkgPath string) string { return string(gno.DerivePkgAddr(pkgPath).Bech32()) } diff --git a/gnovm/stdlibs/stdshim/coins.gno b/gnovm/stdlibs/stdshim/coins.gno index aaede81309d..4589113bff4 100644 --- a/gnovm/stdlibs/stdshim/coins.gno +++ b/gnovm/stdlibs/stdshim/coins.gno @@ -2,8 +2,7 @@ package std import "strconv" -// NOTE: this is selectly copied over from pkgs/std/coin.go -// TODO: import all functionality(?). +// NOTE: this is selectively copied over from tm2/pkgs/std/coin.go // Coin hold some amount of one currency. // A negative amount is invalid. @@ -12,21 +11,115 @@ type Coin struct { Amount int64 `json:"amount"` } +// NewCoin returns a new coin with a denomination and amount +func NewCoin(denom string, amount int64) Coin { + return Coin{ + Denom: denom, + Amount: amount, + } +} + +// String provides a human-readable representation of a coin func (c Coin) String() string { return strconv.Itoa(int(c.Amount)) + c.Denom } +// IsGTE returns true if they are the same type and the receiver is +// an equal or greater value func (c Coin) IsGTE(other Coin) bool { - if c.Denom != other.Denom { - panic("invalid coin denominations: " + c.Denom) - } + mustMatchDenominations(c.Denom, other.Denom) + return c.Amount >= other.Amount } +// IsLT returns true if they are the same type and the receiver is +// a smaller value +func (c Coin) IsLT(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount < other.Amount +} + +// IsEqual returns true if the two sets of Coins have the same value +func (c Coin) IsEqual(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount == other.Amount +} + +// Add adds amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Add(coinB Coin) Coin { + mustMatchDenominations(c.Denom, coinB.Denom) + + sum := c.Amount + coinB.Amount + + c.Amount = sum + return c +} + +// Sub subtracts amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Sub(coinB Coin) Coin { + mustMatchDenominations(c.Denom, coinB.Denom) + + dff := c.Amount - coinB.Amount + c.Amount = dff + + return c +} + +// IsPositive returns true if coin amount is positive. +func (c Coin) IsPositive() bool { + return c.Amount > 0 +} + +// IsNegative returns true if the coin amount is negative and false otherwise. +func (c Coin) IsNegative() bool { + return c.Amount < 0 +} + +// IsZero returns if this represents no money +func (c Coin) IsZero() bool { + return c.Amount == 0 +} + +func mustMatchDenominations(denomA, denomB string) { + if denomA != denomB { + panic("incompatible coin denominations: " + denomA + ", " + denomB) + } +} + // Coins is a set of Coin, one per currency type Coins []Coin +// NewCoins returns a new set of Coins given one or more Coins +// Consolidates any denom duplicates into one, keeping the properties of a mathematical set +func NewCoins(coins ...Coin) Coins { + coinMap := make(map[string]int64) + + for _, coin := range coins { + coinMap[coin.Denom] = coin.Amount + } + + var setCoins Coins + for denom, amount := range coinMap { + setCoins = append(setCoins, NewCoin(denom, amount)) + } + + return setCoins +} + +// String returns the string representation of Coins func (cz Coins) String() string { + if len(cz) == 0 { + return "" + } + res := "" for i, c := range cz { if i > 0 { @@ -34,32 +127,48 @@ func (cz Coins) String() string { } res += c.String() } + return res } +// AmountOf returns the amount of a specific coin from the Coins set func (cz Coins) AmountOf(denom string) int64 { for _, c := range cz { if c.Denom == denom { return c.Amount } } + return 0 } -func (a Coins) Add(b Coins) Coins { +// Add adds a Coin to the Coins set +func (cz Coins) Add(b Coins) Coins { c := Coins{} - for _, ac := range a { + for _, ac := range cz { bc := b.AmountOf(ac.Denom) ac.Amount += bc c = append(c, ac) } + for _, bc := range b { cc := c.AmountOf(bc.Denom) if cc == 0 { c = append(c, bc) } } + return c } -// TODO implement Coin/Coins constructors. +// expandNative expands for usage within natively bound functions. +func (cz Coins) expandNative() (denoms []string, amounts []int64) { + denoms = make([]string, len(cz)) + amounts = make([]int64, len(cz)) + for i, coin := range cz { + denoms[i] = coin.Denom + amounts[i] = coin.Amount + } + + return denoms, amounts +} diff --git a/gnovm/stdlibs/stdshim/stdshim.gno b/gnovm/stdlibs/stdshim/stdshim.gno index 62e97088209..34449f96dd7 100644 --- a/gnovm/stdlibs/stdshim/stdshim.gno +++ b/gnovm/stdlibs/stdshim/stdshim.gno @@ -16,11 +16,6 @@ func Hash(bz []byte) (hash [20]byte) { return } -func CurrentRealmPath() string { - panic(shimWarn) - return "" -} - func GetChainID() string { panic(shimWarn) return "" @@ -84,3 +79,7 @@ func DecodeBech32(addr Address) (prefix string, bytes [20]byte, ok bool) { func DerivePkgAddr(pkgPath string) (addr Address) { panic(shimWarn) } + +func Emit(tag, key, value string) { + panic(shimWarn) +} diff --git a/gnovm/stdlibs/testing/fuzz.gno b/gnovm/stdlibs/testing/fuzz.gno index 62a36800b2b..74d64c231b6 100644 --- a/gnovm/stdlibs/testing/fuzz.gno +++ b/gnovm/stdlibs/testing/fuzz.gno @@ -1,10 +1,6 @@ package testing -import ( - "math" - "strings" - "time" -) +import "strings" type Fuzzer interface { InsertDeleteMutate(p float64) Fuzzer @@ -242,10 +238,7 @@ func (f *F) evolve(generations int) { newPopulation = append(newPopulation, population[i]) } - var ( - bestFitness int - bestIndividual string - ) + var bestFitness int for _, ind := range newPopulation { if GenerateRandomBool(0.2) { @@ -260,7 +253,6 @@ func (f *F) evolve(generations int) { if ind.Fitness > bestFitness { bestFitness = ind.Fitness - bestIndividual = ind.Fuzzer.String() } } diff --git a/gnovm/stdlibs/testing/fuzz_test.gno b/gnovm/stdlibs/testing/fuzz_test.gno index 454c1cccd6e..307148c09b3 100644 --- a/gnovm/stdlibs/testing/fuzz_test.gno +++ b/gnovm/stdlibs/testing/fuzz_test.gno @@ -1,10 +1,6 @@ package testing -import ( - "encoding/binary" - "strings" - "time" -) +import "strings" func TestMutate(t *T) { originalValue := "Hello" diff --git a/gnovm/stdlibs/testing/match.gno b/gnovm/stdlibs/testing/match.gno index 5b7f509d8f6..3b22602890d 100644 --- a/gnovm/stdlibs/testing/match.gno +++ b/gnovm/stdlibs/testing/match.gno @@ -9,7 +9,6 @@ package testing import ( "fmt" "regexp" - "strconv" "strings" "unicode" ) diff --git a/gnovm/stdlibs/testing/random.gno b/gnovm/stdlibs/testing/random.gno index b7ee6ca93ef..9d55abb6e47 100644 --- a/gnovm/stdlibs/testing/random.gno +++ b/gnovm/stdlibs/testing/random.gno @@ -1,9 +1,6 @@ package testing -import ( - "math" - "time" -) +import "math" // Internal state for the random number generator. var x uint64 = 42 diff --git a/gnovm/stdlibs/testing/testing.gno b/gnovm/stdlibs/testing/testing.gno index fb13c0f39cd..7192a2677d9 100644 --- a/gnovm/stdlibs/testing/testing.gno +++ b/gnovm/stdlibs/testing/testing.gno @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "regexp" "strconv" "strings" ) diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 328d9e09299..3394937f9c7 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -136,14 +136,13 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { defer func() { if r := recover(); r != nil { // print output. - fmt.Println("OUTPUT:\n", stdout.String()) - // print stack if unexpected error. + fmt.Printf("OUTPUT:\n%s\n", stdout.String()) pnc = r - if errWanted == "" { - rtdb.PrintStack() - } err := strings.TrimSpace(fmt.Sprintf("%v", pnc)) - if !strings.Contains(err, errWanted) { + // print stack if unexpected error. + if errWanted == "" || + !strings.Contains(err, errWanted) { + fmt.Printf("ERROR:\n%s\n", err) // error didn't match: print stack // NOTE: will fail testcase later. rtdb.PrintStack() diff --git a/gnovm/tests/files/op7.gno b/gnovm/tests/files/op7.gno index c92a567110d..7167171035b 100644 --- a/gnovm/tests/files/op7.gno +++ b/gnovm/tests/files/op7.gno @@ -14,4 +14,4 @@ func main() { } // Error: -// main/files/op7.gno:11: incompatible types in binary expression: err GTR invalidT +// main/files/op7.gno:11: incompatible types in binary expression: .uverse.error GTR main.T diff --git a/gnovm/tests/files/time16_native.gno b/gnovm/tests/files/time16_native.gno new file mode 100644 index 00000000000..1789fcbef4f --- /dev/null +++ b/gnovm/tests/files/time16_native.gno @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + var a int64 = 2 + fmt.Println(time.Second * a) +} + +// Error: +// main/files/time16_native.gno:10: incompatible types in binary expression: go:time.Duration MUL int64 diff --git a/gnovm/tests/files/type31.gno b/gnovm/tests/files/type31.gno index a80d07d70f2..a613c67525c 100644 --- a/gnovm/tests/files/type31.gno +++ b/gnovm/tests/files/type31.gno @@ -9,4 +9,4 @@ func main() { } // Error: -// main/files/type31.gno:8: incompatible types in binary expression: x ADD y +// main/files/type31.gno:8: incompatible types in binary expression: string ADD main.String diff --git a/gnovm/tests/files/type32.gno b/gnovm/tests/files/type32.gno index b679543cf9f..bc943d90186 100644 --- a/gnovm/tests/files/type32.gno +++ b/gnovm/tests/files/type32.gno @@ -12,4 +12,4 @@ func main() { } // Error: -// main/files/type32.gno:9#1: incompatible types in binary expression: a + (const (":" string)) ADD b +// main/files/type32.gno:9#1: incompatible types in binary expression: string ADD main.S diff --git a/gnovm/tests/files/zrealm12.gno b/gnovm/tests/files/zrealm12.gno index 0049cba6073..ee9e85d827b 100644 --- a/gnovm/tests/files/zrealm12.gno +++ b/gnovm/tests/files/zrealm12.gno @@ -3,31 +3,23 @@ package test import ( "std" - - "gno.land/r/demo/tests" ) func main() { - if std.TestCurrentRealm() != "gno.land/r/test" { - panic("should not happen") - } tests.InitTestNodes() - if std.TestCurrentRealm() != "gno.land/r/test" { + if std.CurrentRealm().PkgPath() != "gno.land/r/test" { panic("should not happen") } tests.ModTestNodes() - if std.TestCurrentRealm() != "gno.land/r/test" { + if std.CurrentRealm().PkgPath() != "gno.land/r/test" { panic("should not happen") } std.ClearStoreCache() - if std.TestCurrentRealm() != "gno.land/r/test" { + if std.CurrentRealm().PkgPath() != "gno.land/r/test" { panic("should not happen") } tests.PrintTestNodes() - if std.TestCurrentRealm() != "gno.land/r/test" { + if std.CurrentRealm().PkgPath() != "gno.land/r/test" { panic("should not happen") } } - -// Output: -// second's child diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 084ddd3d18f..3808905928b 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -12,19 +12,19 @@ func init() { } func main() { - // NOTE: this test uses GetHeight and CurrentRealmPath, which are "pure" + // NOTE: this test uses GetHeight and GetChainID, which are "pure" // natively bound functions (ie. not indirections through a wrapper fn, // to convert the types to builtin go/gno identifiers). f := node.(func() int64) println(f()) - node = std.CurrentRealmPath + node = std.GetChainID g := node.(func() string) println(g()) } // Output: // 123 -// gno.land/r/test +// dev // Realm: // switchrealm["gno.land/r/test"] @@ -141,12 +141,12 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:7" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:8" // }, // "FileName": "native.gno", // "IsMethod": false, -// "Name": "CurrentRealmPath", -// "NativeName": "CurrentRealmPath", +// "Name": "GetChainID", +// "NativeName": "GetChainID", // "NativePkg": "std", // "PkgPath": "std", // "Source": { diff --git a/gnovm/tests/files/zrealm_std3.gno b/gnovm/tests/files/zrealm_std3.gno index c13feffa42c..4f1d1bc827a 100644 --- a/gnovm/tests/files/zrealm_std3.gno +++ b/gnovm/tests/files/zrealm_std3.gno @@ -6,11 +6,11 @@ import ( ) func foo() { - println("foo", std.CurrentRealmPath()) + println("foo", std.CurrentRealm().PkgPath()) } func main() { - println("main", std.CurrentRealmPath()) + println("main", std.CurrentRealm().PkgPath()) foo() } diff --git a/gnovm/tests/imports.go b/gnovm/tests/imports.go index d5541fb0554..d5e63532875 100644 --- a/gnovm/tests/imports.go +++ b/gnovm/tests/imports.go @@ -29,7 +29,6 @@ import ( "os" "path/filepath" "reflect" - "sort" "strconv" "strings" "sync" @@ -258,16 +257,16 @@ func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Wri pkg.DefineGoNativeValue("Pi", math.Pi) pkg.DefineGoNativeValue("MaxFloat32", math.MaxFloat32) pkg.DefineGoNativeValue("MaxFloat64", math.MaxFloat64) - pkg.DefineGoNativeValue("MaxUint32", math.MaxUint32) + pkg.DefineGoNativeValue("MaxUint32", uint32(math.MaxUint32)) pkg.DefineGoNativeValue("MaxUint64", uint64(math.MaxUint64)) pkg.DefineGoNativeValue("MinInt8", math.MinInt8) pkg.DefineGoNativeValue("MinInt16", math.MinInt16) pkg.DefineGoNativeValue("MinInt32", math.MinInt32) - pkg.DefineGoNativeValue("MinInt64", math.MinInt64) + pkg.DefineGoNativeValue("MinInt64", int64(math.MinInt64)) pkg.DefineGoNativeValue("MaxInt8", math.MaxInt8) pkg.DefineGoNativeValue("MaxInt16", math.MaxInt16) pkg.DefineGoNativeValue("MaxInt32", math.MaxInt32) - pkg.DefineGoNativeValue("MaxInt64", math.MaxInt64) + pkg.DefineGoNativeValue("MaxInt64", int64(math.MaxInt64)) return pkg, pkg.NewPackage() case "math/rand": // XXX only expose for tests. @@ -332,11 +331,6 @@ func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Wri pkg := gno.NewPackageNode("big", pkgPath, nil) pkg.DefineGoNativeValue("NewInt", big.NewInt) return pkg, pkg.NewPackage() - case "sort": - pkg := gno.NewPackageNode("sort", pkgPath, nil) - pkg.DefineGoNativeValue("Strings", sort.Strings) - // pkg.DefineGoNativeValue("Sort", sort.Sort) - return pkg, pkg.NewPackage() case "flag": pkg := gno.NewPackageNode("flag", pkgPath, nil) pkg.DefineGoNativeType(reflect.TypeOf(flag.Flag{})) diff --git a/gnovm/tests/integ/several-lint-errors/gno.mod b/gnovm/tests/integ/several-lint-errors/gno.mod new file mode 100644 index 00000000000..88485411822 --- /dev/null +++ b/gnovm/tests/integ/several-lint-errors/gno.mod @@ -0,0 +1 @@ +module gno.land/tests/severalerrors \ No newline at end of file diff --git a/gnovm/tests/integ/several-lint-errors/main.gno b/gnovm/tests/integ/several-lint-errors/main.gno new file mode 100644 index 00000000000..f29aa7ecd33 --- /dev/null +++ b/gnovm/tests/integ/several-lint-errors/main.gno @@ -0,0 +1,6 @@ +package main + +func main() { + for { + _ example +} \ No newline at end of file diff --git a/gnovm/tests/package_test.go b/gnovm/tests/package_test.go index a3f9e587030..ded5bedce5f 100644 --- a/gnovm/tests/package_test.go +++ b/gnovm/tests/package_test.go @@ -7,7 +7,6 @@ import ( "log" "os" "path/filepath" - "sort" "strings" "testing" @@ -16,11 +15,14 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" ) -func TestPackages(t *testing.T) { +func TestStdlibs(t *testing.T) { + // NOTE: this test only works using _test.gno files; + // filetests are not meant to be used for testing standard libraries. + // The examples directory is tested directly using `gno test`. + // find all packages with *_test.gno files. rootDirs := []string{ filepath.Join("..", "stdlibs"), - filepath.Join("..", "..", "examples"), } testDirs := map[string]string{} // aggregate here, pkgPath -> dir pkgPaths := []string{} @@ -45,114 +47,10 @@ func TestPackages(t *testing.T) { return nil }) } - // Sort pkgPaths for determinism. - sort.Strings(pkgPaths) // For each package with testfiles (in testDirs), call Machine.TestMemPackage. for _, pkgPath := range pkgPaths { testDir := testDirs[pkgPath] t.Run(pkgPath, func(t *testing.T) { - t.Skip("almost any new package is failing. Ignoring this test for now until we find a solution for this.") - - if pkgPath == "gno.land/p/demo/avl" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/flow" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/grc/exts/vault" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/grc/grc1155" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/grc/grc20" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/grc/grc721" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/memeland" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/ownable" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/pausable" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/rand" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/tests" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/p/demo/todolist" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/art/gnoface" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/foo1155" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/foo20" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/keystore" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/microblog" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/tests" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/todolist" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/demo/userbook" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/gnoland/blog" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/gnoland/faucet" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/x/manfred_outfmt" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/x/nir1218_evaluation_proposal" { - t.Skip("package failing") - } - - if pkgPath == "gno.land/r/gnoland/ghverify" { - t.Skip("package failing") - } - runPackageTest(t, testDir, pkgPath) }) } diff --git a/gnovm/tests/stdlibs/native.go b/gnovm/tests/stdlibs/native.go index 81100838784..6d0e3caa1f1 100644 --- a/gnovm/tests/stdlibs/native.go +++ b/gnovm/tests/stdlibs/native.go @@ -50,25 +50,6 @@ var nativeFuncs = [...]nativeFunc{ )) }, }, - { - "std", - "TestCurrentRealm", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("string")}, - }, - func(m *gno.Machine) { - r0 := testlibs_std.TestCurrentRealm( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "TestSkipHeights", diff --git a/gnovm/tests/stdlibs/std/std.gno b/gnovm/tests/stdlibs/std/std.gno index 0a4f9cc6eff..91c47fed2fd 100644 --- a/gnovm/tests/stdlibs/std/std.gno +++ b/gnovm/tests/stdlibs/std/std.gno @@ -2,7 +2,6 @@ package std func AssertOriginCall() // injected func IsOriginCall() bool // injected -func TestCurrentRealm() string // injected func TestSkipHeights(count int64) // injected func ClearStoreCache() // injected @@ -15,6 +14,7 @@ func TestSetOrigSend(sent, spent Coins) { spentDenom, spentAmt := spent.expandNative() testSetOrigSend(sentDenom, sentAmt, spentDenom, spentAmt) } + func TestIssueCoins(addr Address, coins Coins) { denom, amt := coins.expandNative() testIssueCoins(string(addr), denom, amt) diff --git a/gnovm/tests/stdlibs/std/std.go b/gnovm/tests/stdlibs/std/std.go index 72a2a7734ed..f5c30a08868 100644 --- a/gnovm/tests/stdlibs/std/std.go +++ b/gnovm/tests/stdlibs/std/std.go @@ -41,10 +41,6 @@ func IsOriginCall(m *gno.Machine) bool { panic("unable to determine if test is a _test or a _filetest") } -func TestCurrentRealm(m *gno.Machine) string { - return m.Realm.Path -} - func TestSkipHeights(m *gno.Machine, count int64) { ctx := m.Context.(std.ExecContext) ctx.Height += count diff --git a/go.mod b/go.mod index b5ef5021fac..76c42f0419c 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 golang.org/x/mod v0.16.0 golang.org/x/net v0.23.0 + golang.org/x/sync v0.6.0 golang.org/x/term v0.18.0 golang.org/x/tools v0.19.0 google.golang.org/protobuf v1.33.0 diff --git a/misc/autocounterd/README.md b/misc/autocounterd/README.md index 28b44312f25..b844a3d9e6c 100644 --- a/misc/autocounterd/README.md +++ b/misc/autocounterd/README.md @@ -2,7 +2,7 @@ ## What is it? -`autcounterd` is a simple binary that increments a simple counter every x seconds to analyse and test a network. +`autocounterd` is a simple binary that increments a simple counter every x seconds to analyse and test a network. ## How to use diff --git a/misc/deployments/staging.gno.land/docker-compose.yml b/misc/deployments/staging.gno.land/docker-compose.yml index 991a102b691..3479067372d 100644 --- a/misc/deployments/staging.gno.land/docker-compose.yml +++ b/misc/deployments/staging.gno.land/docker-compose.yml @@ -6,7 +6,7 @@ services: build: ../../.. environment: - VIRTUAL_HOST=rpc.staging.gno.land - - VIRTUAL_PORT=36657 + - VIRTUAL_PORT=26657 - LETSENCRYPT_HOST=rpc.staging.gno.land - LOG_LEVEL=4 working_dir: /opt/gno/src/gno.land @@ -15,12 +15,12 @@ services: - start - --skip-failing-genesis-txs - --chainid=staging - - --genesis-remote=staging.gno.land:36657 + - --genesis-remote=staging.gno.land:26657 volumes: - "./data/gnoland:/opt/gno/src/gno.land/gnoland-data" ports: - - 36656:36656 - - 36657:36657 + - 26656:26656 + - 26657:26657 restart: on-failure logging: driver: "json-file" @@ -34,11 +34,11 @@ services: command: - gnoweb - --bind=0.0.0.0:80 - - --remote=gnoland:36657 + - --remote=gnoland:26657 - --captcha-site=$RECAPTCHA_SITE_KEY - --faucet-url=https://faucet-staging.gno.land/ - --help-chainid=staging - - --help-remote=staging.gno.land:36657 + - --help-remote=staging.gno.land:26657 - --with-analytics volumes: - "./overlay:/overlay:ro" @@ -64,7 +64,7 @@ services: mkdir -p /.gno && expect -c \"set timeout -1; spawn gnokey add --home /.gno/ --recover faucet; expect \\\"Enter a passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Repeat the passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Enter your bip39 mnemonic\\\"; send \\\"$FAUCET_WORDS\\r\\\"; expect eof\" && while true; do - expect -c \"set timeout -1; spawn gnofaucet serve --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:36657 --chain-id staging --home /.gno/ faucet; expect \\\"Enter password\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; + expect -c \"set timeout -1; spawn gnofaucet serve --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:26657 --chain-id staging --home /.gno/ faucet; expect \\\"Enter password\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; sleep 5; done " diff --git a/misc/deployments/staging.gno.land/overlay/config.toml b/misc/deployments/staging.gno.land/overlay/config.toml index 1f5952f9638..89746e75c4f 100644 --- a/misc/deployments/staging.gno.land/overlay/config.toml +++ b/misc/deployments/staging.gno.land/overlay/config.toml @@ -72,7 +72,7 @@ filter_peers = false [rpc] # TCP or UNIX socket address for the RPC server to listen on -laddr = "tcp://0.0.0.0:36657" +laddr = "tcp://0.0.0.0:26657" # A list of origins a cross-domain request can be executed from # Default value '[]' disables cors support @@ -139,7 +139,7 @@ tls_key_file = "" [p2p] # Address to listen for incoming connections -laddr = "tcp://0.0.0.0:36656" +laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial # If empty, will use the same port as the laddr, diff --git a/misc/deployments/test2.gno.land/Makefile b/misc/deployments/test2.gno.land/Makefile index ad334f31243..a77a81f62d0 100644 --- a/misc/deployments/test2.gno.land/Makefile +++ b/misc/deployments/test2.gno.land/Makefile @@ -25,9 +25,9 @@ build: gnotxport: #docker build -t gno ../.. - docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport export --remote test2.gno.land:36657 + docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport export --remote test2.gno.land:26657 tail -n 1 txexport.log wc -l txexport.log gnotximport: - docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport import --remote test2.gno.land:36657 + docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport import --remote test2.gno.land:26657 diff --git a/misc/deployments/test2.gno.land/docker-compose.yml b/misc/deployments/test2.gno.land/docker-compose.yml index c836c0cc65f..ad613b3f246 100644 --- a/misc/deployments/test2.gno.land/docker-compose.yml +++ b/misc/deployments/test2.gno.land/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: - LOG_LEVEL=4 - VIRTUAL_HOST=rpc.test2.gno.land - - VIRTUAL_PORT=36657 + - VIRTUAL_PORT=26657 - LETSENCRYPT_HOST=rpc.test2.gno.land command: - gnoland @@ -16,15 +16,15 @@ services: - --chainid - test2 - --genesis-remote - - test2.gno.land:36657 + - test2.gno.land:26657 - --genesis-balances-file - /overlay/balances.txt volumes: - "./data/gnoland:/opt/gno/src/testdir" - "./overlay:/overlay:ro" ports: - - 36656:36656 - - 36657:36657 + - 26656:26656 + - 26657:26657 restart: on-failure logging: driver: "json-file" @@ -40,7 +40,7 @@ services: - --bind - 0.0.0.0:80 - --remote - - gnoland:36657 + - gnoland:26657 - --captcha-site - $RECAPTCHA_SITE_KEY - --faucet-url @@ -48,7 +48,7 @@ services: - --help-chainid - test2 - --help-remote - - test2.gno.land:36657 + - test2.gno.land:26657 - --home-content - /overlay/HOME.md volumes: @@ -75,7 +75,7 @@ services: mkdir -p /.gno && expect -c \"set timeout -1; spawn gnokey add faucet --home /.gno/ --recover; expect \\\"Enter a passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Repeat the passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Enter your bip39 mnemonic\\\"; send \\\"$FAUCET_WORDS\\r\\\"; expect eof\" && while true; do - expect -c \"set timeout -1; spawn gnofaucet serve faucet --is-behind-proxy --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:36657 --chain-id test2 --home /.gno/; expect \\\"Enter password.\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; + expect -c \"set timeout -1; spawn gnofaucet serve faucet --is-behind-proxy --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:26657 --chain-id test2 --home /.gno/; expect \\\"Enter password.\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; sleep 5; done " diff --git a/misc/deployments/test2.gno.land/overlay/config.toml b/misc/deployments/test2.gno.land/overlay/config.toml index 65e34a2c55c..ac34760de62 100644 --- a/misc/deployments/test2.gno.land/overlay/config.toml +++ b/misc/deployments/test2.gno.land/overlay/config.toml @@ -72,7 +72,7 @@ filter_peers = false [rpc] # TCP or UNIX socket address for the RPC server to listen on -laddr = "tcp://0.0.0.0:36657" +laddr = "tcp://0.0.0.0:26657" # A list of origins a cross-domain request can be executed from # Default value '[]' disables cors support @@ -139,7 +139,7 @@ tls_key_file = "" [p2p] # Address to listen for incoming connections -laddr = "tcp://0.0.0.0:36656" +laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial # If empty, will use the same port as the laddr, diff --git a/misc/deployments/test3.gno.land/Makefile b/misc/deployments/test3.gno.land/Makefile index d52b95b260b..5d998e8cc6f 100644 --- a/misc/deployments/test3.gno.land/Makefile +++ b/misc/deployments/test3.gno.land/Makefile @@ -28,9 +28,9 @@ build: gnotxport: #docker build -t gno ../.. - docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport export --remote test3.gno.land:36657 + docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport export --remote test3.gno.land:26657 tail -n 1 txexport.log wc -l txexport.log gnotximport: - docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport import --remote test3.gno.land:36657 + docker run -v "$(PWD):$(PWD)" -w "$(PWD)" gno gnotxport import --remote test3.gno.land:26657 diff --git a/misc/deployments/test3.gno.land/docker-compose.yml b/misc/deployments/test3.gno.land/docker-compose.yml index b76d5c3e379..62adda7cc3e 100644 --- a/misc/deployments/test3.gno.land/docker-compose.yml +++ b/misc/deployments/test3.gno.land/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: - LOG_LEVEL=4 - VIRTUAL_HOST=rpc.test3.gno.land - - VIRTUAL_PORT=36657 + - VIRTUAL_PORT=26657 - LETSENCRYPT_HOST=rpc.test3.gno.land command: - gnoland @@ -16,7 +16,7 @@ services: - --chainid - test3 - --genesis-remote - - test3.gno.land:36657 + - test3.gno.land:26657 - --genesis-balances-file - /overlay/balances.txt - --genesis-txs-file @@ -25,8 +25,9 @@ services: - "./data/gnoland:/opt/gno/src/testdir" - "./overlay:/overlay:ro" ports: - - 36656:36656 - - 36657:36657 + - 26656:26656 + - 26657:26657 + - 36657:26657 restart: on-failure logging: driver: "json-file" @@ -42,7 +43,7 @@ services: - --bind - 0.0.0.0:80 - --remote - - gnoland:36657 + - gnoland:26657 - --captcha-site - $RECAPTCHA_SITE_KEY - --faucet-url @@ -50,7 +51,7 @@ services: - --help-chainid - test3 - --help-remote - - test3.gno.land:36657 + - test3.gno.land:26657 - --home-content - /overlay/HOME.md volumes: @@ -77,7 +78,7 @@ services: mkdir -p /.gno && expect -c \"set timeout -1; spawn gnokey add faucet --home /.gno/ --recover; expect \\\"Enter a passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Repeat the passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Enter your bip39 mnemonic\\\"; send \\\"$FAUCET_WORDS\\r\\\"; expect eof\" && while true; do - expect -c \"set timeout -1; spawn gnofaucet serve faucet --is-behind-proxy --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:36657 --chain-id test3 --home /.gno/; expect \\\"Enter password.\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; + expect -c \"set timeout -1; spawn gnofaucet serve faucet --is-behind-proxy --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:26657 --chain-id test3 --home /.gno/; expect \\\"Enter password.\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; sleep 5; done " diff --git a/misc/deployments/test3.gno.land/overlay/config.toml b/misc/deployments/test3.gno.land/overlay/config.toml index 65e34a2c55c..ac34760de62 100644 --- a/misc/deployments/test3.gno.land/overlay/config.toml +++ b/misc/deployments/test3.gno.land/overlay/config.toml @@ -72,7 +72,7 @@ filter_peers = false [rpc] # TCP or UNIX socket address for the RPC server to listen on -laddr = "tcp://0.0.0.0:36657" +laddr = "tcp://0.0.0.0:26657" # A list of origins a cross-domain request can be executed from # Default value '[]' disables cors support @@ -139,7 +139,7 @@ tls_key_file = "" [p2p] # Address to listen for incoming connections -laddr = "tcp://0.0.0.0:36656" +laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial # If empty, will use the same port as the laddr, diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index adba5dca170..ea58f93ed7d 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -71,7 +71,7 @@ func NewSnapshotter(dockerClient *client.Client, cfg config) (*snapshotter, erro // pullLatestImage get latest version of the docker image func (s snapshotter) pullLatestImage(ctx context.Context) (bool, error) { - reader, err := s.dockerClient.ImagePull(ctx, "ghcr.io/gnolang/gno", types.ImagePullOptions{}) + reader, err := s.dockerClient.ImagePull(ctx, "ghcr.io/gnolang/gno/gnoland", types.ImagePullOptions{}) if err != nil { return false, err } @@ -139,15 +139,16 @@ func (s snapshotter) startPortalLoopContainer(ctx context.Context) (*types.Conta // Run Docker container container, err := s.dockerClient.ContainerCreate(ctx, &container.Config{ - Image: "ghcr.io/gnolang/gno", + Image: "ghcr.io/gnolang/gno/gnoland", Labels: map[string]string{ "the-portal-loop": s.containerName, }, + WorkingDir: "/gnoroot", Env: []string{ "MONIKER=the-portal-loop", "GENESIS_BACKUP_FILE=/backups/backup.jsonl", }, - Cmd: []string{"/scripts/start.sh"}, + Entrypoint: []string{"/scripts/start.sh"}, ExposedPorts: nat.PortSet{ "26656/tcp": struct{}{}, "26657/tcp": struct{}{}, @@ -162,7 +163,7 @@ func (s snapshotter) startPortalLoopContainer(ctx context.Context) (*types.Conta Binds: []string{ fmt.Sprintf("%s/scripts:/scripts", s.cfg.hostPWD), fmt.Sprintf("%s/backups:/backups", s.cfg.hostPWD), - fmt.Sprintf("%s:/opt/gno/src/gnoland-data", s.containerName), + fmt.Sprintf("%s:/gnoroot/gnoland-data", s.containerName), }, }, nil, nil, s.containerName) if err != nil { diff --git a/misc/loop/docker-compose.production.yml b/misc/loop/docker-compose.production.yml index 3873768dec6..4b0a316cb5a 100644 --- a/misc/loop/docker-compose.production.yml +++ b/misc/loop/docker-compose.production.yml @@ -117,7 +117,7 @@ services: - "com.centurylinklabs.watchtower.enable=true" autocounterd: - image: ghcr.io/albttx/gno/autocounterd + image: ghcr.io/gnolang/gno/autocounterd restart: unless-stopped env_file: ".env" command: diff --git a/misc/loop/scripts/start.sh b/misc/loop/scripts/start.sh index 1b52f6f03f5..76869ccb4bd 100755 --- a/misc/loop/scripts/start.sh +++ b/misc/loop/scripts/start.sh @@ -11,19 +11,26 @@ GENESIS_BACKUP_FILE=${GENESIS_BACKUP_FILE:-""} SEEDS=${SEEDS:-""} PERSISTENT_PEERS=${PERSISTENT_PEERS:-""} -echo "" >> /opt/gno/src/gno.land/genesis/genesis_txs.jsonl -cat ${GENESIS_BACKUP_FILE} >> /opt/gno/src/gno.land/genesis/genesis_txs.jsonl - -gnoland start \ - --chainid="${CHAIN_ID}" \ - --skip-start=true \ - --skip-failing-genesis-txs - -sed -i "s#^moniker = \".*\"#moniker = \"${MONIKER}\"#" ./gnoland-data/config/config.toml -sed -i "s#laddr = \".*:26656\"#laddr = \"${P2P_LADDR}\"#" ./gnoland-data/config/config.toml -sed -i "s#laddr = \".*:26657\"#laddr = \"${RPC_LADDR}\"#" ./gnoland-data/config/config.toml - -sed -i "s#seeds = \".*\"#seeds = \"${SEEDS}\"#" ./gnoland-data/config/config.toml -sed -i "s#persistent_peers = \".*\"#persistent_peers = \"${PERSISTENT_PEERS}\"#" ./gnoland-data/config/config.toml - -exec gnoland start --skip-failing-genesis-txs +echo "" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +cat ${GENESIS_BACKUP_FILE} >> /gnoroot/gno.land/genesis/genesis_txs.jsonl + +# Initialize the secrets +gnoland secrets init + +# Initialize the configuration +gnoland config init + +# Set the config values +gnoland config set moniker "${MONIKER}" +gnoland config set rpc.laddr "${RPC_LADDR}" +gnoland config set p2p.laddr "${P2P_LADDR}" +gnoland config set p2p.seeds "${SEEDS}" +gnoland config set p2p.persistent_peers "${PERSISTENT_PEERS}" + +# Running a lazy init will generate a fresh genesis.json, with +# the previously generated secrets. We do this to avoid CLI magic from config +# reading and piping to the gnoland genesis commands +exec gnoland start \ + --chainid="${CHAIN_ID}" \ + --lazy \ + --skip-failing-genesis-txs diff --git a/misc/telemetry/Makefile b/misc/telemetry/Makefile new file mode 100644 index 00000000000..956c4f11190 --- /dev/null +++ b/misc/telemetry/Makefile @@ -0,0 +1,11 @@ +.PHONY: up +up: + docker compose up -d --build + +.PHONY: down +down: + docker compose down + +.PHONY: clean +clean: + docker compose down -v \ No newline at end of file diff --git a/misc/telemetry/README.md b/misc/telemetry/README.md new file mode 100644 index 00000000000..41628cc5f51 --- /dev/null +++ b/misc/telemetry/README.md @@ -0,0 +1,56 @@ +## Overview + +The purpose of this Telemetry documentation is to showcase the different node metrics exposed by the Gno node through +OpenTelemetry, without having to do extraneous setup. + +The containerized setup is the following: + +- Grafana dashboard +- Prometheus +- OpenTelemetry collector (separate service that needs to run) +- Single Gnoland node, with 1s block times and configured telemetry (enabled) +- Supernova process that simulates load periodically (generates network traffic) + +## Starting the containers + +### Step 1: Spinning up Docker + +Make sure you have Docker installed and running on your system. After that, within the `misc/telemetry` folder run the +following command: + +```shell +make up +``` + +This will build out the required Docker images for this simulation, and start the services + +### Step 2: Open Grafana + +When you've verified that the `telemetry` containers are up and running, head on over to http://localhost:3000 to open +the Grafana dashboard. + +Default login details: + +``` +username: admin +password: admin +``` + +After you've logged in (you can skip setting a new password), on the left hand side, click on +`Dashboards -> Gno -> Gno Node Metrics`: +![Grafana](assets/grafana-1.jpeg) + +This will open up the predefined Gno Metrics dashboards (added for ease of use) : +![Metrics Dashboard](assets/grafana-2.jpeg) + +Periodically, these metrics will be updated as the `supernova` process is simulating network traffic. + +### Step 3: Stopping the cluster + +To stop the cluster, you can run: + +```shell +make down +``` + +which will stop the Docker containers. Additionally, you can delete the Docker volumes with `make clean`. \ No newline at end of file diff --git a/misc/telemetry/assets/grafana-1.jpeg b/misc/telemetry/assets/grafana-1.jpeg new file mode 100644 index 00000000000..f29ca02609e Binary files /dev/null and b/misc/telemetry/assets/grafana-1.jpeg differ diff --git a/misc/telemetry/assets/grafana-2.jpeg b/misc/telemetry/assets/grafana-2.jpeg new file mode 100644 index 00000000000..a08e8317078 Binary files /dev/null and b/misc/telemetry/assets/grafana-2.jpeg differ diff --git a/misc/telemetry/collector/collector.yaml b/misc/telemetry/collector/collector.yaml new file mode 100644 index 00000000000..d258dd025bb --- /dev/null +++ b/misc/telemetry/collector/collector.yaml @@ -0,0 +1,22 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +processors: + batch: + +exporters: + prometheus: + endpoint: collector:8090 + +service: + telemetry: + logs: + level: "debug" + pipelines: + metrics: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ prometheus ] diff --git a/misc/telemetry/docker-compose.yml b/misc/telemetry/docker-compose.yml new file mode 100644 index 00000000000..91c2ea3471d --- /dev/null +++ b/misc/telemetry/docker-compose.yml @@ -0,0 +1,65 @@ +services: + collector: + image: otel/opentelemetry-collector-contrib:latest + ports: + - "4317:4317" + - "4318:4318" + - "8090" + volumes: + - ./collector/collector.yaml:/etc/otelcol-contrib/config.yaml + networks: + - gnoland-net + prometheus: + image: prom/prometheus:latest + command: + - "--enable-feature=remote-write-receiver" + - "--config.file=/etc/prometheus/prometheus.yml" + ports: + - "9090:9090" + volumes: + - prometheus:/prometheus + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + - gnoland-net + grafana: + image: grafana/grafana-enterprise + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - ./grafana/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml + - ./grafana/gno-dashboards.json:/var/lib/grafana/dashboards/gno-dashboards.json + ports: + - "3000:3000" + networks: + - gnoland-net + gnoland: + build: + context: ./gnoland + dockerfile: Dockerfile + ports: + - "26657:26657" + networks: + - gnoland-net + supernova: + build: + dockerfile: supernova.Dockerfile + args: + supernova_version: v1.2.1 + command: > + -sub-accounts 10 -transactions 200 -url http://gnoland: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" + restart: always + networks: + - gnoland-net + +networks: + gnoland-net: + driver: bridge + +volumes: + prometheus: + driver: local + grafana_data: + driver: local + gnoland: + driver: local diff --git a/misc/telemetry/gnoland/Dockerfile b/misc/telemetry/gnoland/Dockerfile new file mode 100644 index 00000000000..9166ea4251b --- /dev/null +++ b/misc/telemetry/gnoland/Dockerfile @@ -0,0 +1,11 @@ +# Use the existing gno image as the base image +FROM ghcr.io/gnolang/gno:latest AS base + +# Copy the setup script into the container +COPY ./setup.sh . + +# Make the script executable +RUN chmod +x ./setup.sh + +# Run the setup +CMD ["./setup.sh"] diff --git a/misc/telemetry/gnoland/setup.sh b/misc/telemetry/gnoland/setup.sh new file mode 100644 index 00000000000..5d3e1d259a1 --- /dev/null +++ b/misc/telemetry/gnoland/setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Initialize the node data directories +gnoland start --skip-start + +# Set the block time to 1s +gnoland config set --config-path /opt/gno/src/gnoland-data/config/config.toml consensus.timeout_commit 1s + +# Set the listen address +gnoland config set --config-path /opt/gno/src/gnoland-data/config/config.toml rpc.laddr tcp://0.0.0.0:26657 + +# Enable the metrics +gnoland config set --config-path /opt/gno/src/gnoland-data/config/config.toml telemetry.enabled true + +# Set the metrics exporter endpoint +gnoland config set --config-path /opt/gno/src/gnoland-data/config/config.toml telemetry.exporter_endpoint collector:4317 + +# Start the Gnoland node +gnoland start \ No newline at end of file diff --git a/misc/telemetry/grafana/dashboards.yaml b/misc/telemetry/grafana/dashboards.yaml new file mode 100644 index 00000000000..6a70278b8a1 --- /dev/null +++ b/misc/telemetry/grafana/dashboards.yaml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: + - name: Gno Node Metrics + folder: Gno + type: file + options: + path: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/misc/telemetry/grafana/datasources.yaml b/misc/telemetry/grafana/datasources.yaml new file mode 100644 index 00000000000..917b8a544f1 --- /dev/null +++ b/misc/telemetry/grafana/datasources.yaml @@ -0,0 +1,13 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + orgId: 1 + url: http://prometheus:9090 + basicAuth: false + isDefault: false + version: 1 + editable: true + uid: prometheus diff --git a/misc/telemetry/grafana/gno-dashboards.json b/misc/telemetry/grafana/gno-dashboards.json new file mode 100644 index 00000000000..58373bfb1ee --- /dev/null +++ b/misc/telemetry/grafana/gno-dashboards.json @@ -0,0 +1,1360 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "All Gno node metrics", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 23, + "panels": [], + "title": "VM", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "vm_query_errors_counter", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Number of VM Query Errors", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "vm_query_calls_counter", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Number of VM Query Calls", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 26, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(vm_gas_used_hist_sum[10m])/rate(vm_gas_used_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Gas Used by VM execution [10min]", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 27, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(vm_cpu_cycles_hist_sum[10m])/rate(vm_cpu_cycles_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average CPU Cycles in VM execution [10min]", + "type": "stat" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 15, + "title": "Mempool", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(num_mempool_txs_hist_sum[10m])/rate(num_mempool_txs_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Valid Mempool Txs", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(num_cached_txs_hist_sum[10m])/rate(num_cached_txs_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Cached Mempool Txs", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 11, + "panels": [], + "title": "Networking", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(inbound_peers_hist_sum[10m])/rate(inbound_peers_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Inbound Peer Count", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": -5, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(outbound_peers_hist_sum[10m])/rate(outbound_peers_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Outbound Peer Count", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": -5, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 6, + "y": 35 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(dialing_peers_hist_sum[10m])/rate(dialing_peers_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Dialing Peer Count", + "type": "stat" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 9, + "title": "Consensus", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 44 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(validator_count_hist_sum[10m])/rate(validator_count_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Validator Count", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 44 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(validator_vp_hist_sum[10m])/rate(validator_vp_hist_count[10m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average Total Validator Voting Power", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 52 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(build_block_hist_milliseconds_sum[10m])/rate(build_block_hist_milliseconds_count[10m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average Block Build Time [10min]", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 52 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(block_interval_hist_seconds_sum[10m])/rate(block_interval_hist_seconds_count[10m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average Block Interval [10min]", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 60 + }, + "id": 20, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum(block_txs_hist_bucket\n) by (le)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average Block Tx Count [10min]", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 60 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(block_size_hist_B_sum[10m])/rate(block_size_hist_B_count[10m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average Block Size [10min]", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 8, + "panels": [], + "title": "Misc", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 1000 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(broadcast_tx_hist_milliseconds_sum[10m])/rate(broadcast_tx_hist_milliseconds_count[10m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average Transaction Broadcast Duration [10min]", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 77 + }, + "id": 7, + "panels": [], + "title": "JSON-RPC", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 2000 + }, + { + "color": "red", + "value": 5000 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 78 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(http_request_time_hist_milliseconds_sum[10m])/rate(http_request_time_hist_milliseconds_count[10m])", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average HTTP Request Round Trip Time [10min]", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 2000 + }, + { + "color": "red", + "value": 5000 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 78 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(ws_request_time_hist_milliseconds_sum[10m])/rate(ws_request_time_hist_milliseconds_count[10m])", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Average WS Request Round Trip Time [10min]", + "type": "stat" + } + ], + "refresh": "5s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Gno Node Metrics", + "uid": "bdl7d5yogxjb4b", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/misc/telemetry/prometheus/prometheus.yml b/misc/telemetry/prometheus/prometheus.yml new file mode 100644 index 00000000000..f92b2a56da7 --- /dev/null +++ b/misc/telemetry/prometheus/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'opentelemetry' + static_configs: + - targets: [ 'collector:8090' ] diff --git a/misc/telemetry/supernova.Dockerfile b/misc/telemetry/supernova.Dockerfile new file mode 100644 index 00000000000..67ccbda8047 --- /dev/null +++ b/misc/telemetry/supernova.Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.22-alpine + +ARG supernova_version=latest + +RUN go install github.com/gnolang/supernova/cmd@$supernova_version && mv /go/bin/cmd /go/bin/supernova +RUN export SUPERNOVA_PATH=$(go list -m -f "{{.Dir}}" github.com/gnolang/supernova@${supernova_version}) && \ + mkdir -p /supernova && \ + cp -r $SUPERNOVA_PATH/* /supernova + +WORKDIR /supernova + +ENTRYPOINT ["supernova"] diff --git a/tm2/pkg/amino/genproto/bindings.go b/tm2/pkg/amino/genproto/bindings.go index 5d3b46c59fc..c1de79caa59 100644 --- a/tm2/pkg/amino/genproto/bindings.go +++ b/tm2/pkg/amino/genproto/bindings.go @@ -1100,6 +1100,27 @@ func _fields(args ...interface{}) *ast.FieldList { } } +const ( + reDGTS = `(?:[0-9]+)` + reHExX = `(?:0[xX][0-9a-fA-F]+)` + rePSCI = `(?:[eE]+?[0-9]+)` + reNSCI = `(?:[eE]-[1-9][0-9]+)` + reASCI = `(?:[eE][-+]?[0-9]+)` +) + +var ( + reIsInt = regexp.MustCompile( + `^-?(?:` + + reDGTS + `|` + + reHExX + `)` + rePSCI + `?$`, + ) + reIsFloat = regexp.MustCompile( + `^-?(?:` + + reDGTS + `\.` + reDGTS + reASCI + `?|` + + reDGTS + reNSCI + `)$`, + ) +) + // Parses simple expressions (but not all). // Useful for parsing strings to ast nodes, like foo.bar["qwe"](), // new(bytes.Buffer), *bytes.Buffer, package.MyStruct{FieldA:1}, numeric @@ -1282,22 +1303,7 @@ func _x(expr string, args ...interface{}) ast.Expr { } } // Numeric int? We do these before dots, because dots are legal in numbers. - const ( - DGTS = `(?:[0-9]+)` - HExX = `(?:0[xX][0-9a-fA-F]+)` - PSCI = `(?:[eE]+?[0-9]+)` - NSCI = `(?:[eE]-[1-9][0-9]+)` - ASCI = `(?:[eE][-+]?[0-9]+)` - ) - isInt, err := regexp.Match( - `^-?(?:`+ - DGTS+`|`+ - HExX+`)`+PSCI+`?$`, - []byte(expr), - ) - if err != nil { - panic("should not happen") - } + isInt := reIsInt.MatchString(expr) if isInt { return &ast.BasicLit{ Kind: token.INT, @@ -1305,15 +1311,7 @@ func _x(expr string, args ...interface{}) ast.Expr { } } // Numeric float? We do these before dots, because dots are legal in floats. - isFloat, err := regexp.Match( - `^-?(?:`+ - DGTS+`\.`+DGTS+ASCI+`?|`+ - DGTS+NSCI+`)$`, - []byte(expr), - ) - if err != nil { - panic("should not happen") - } + isFloat := reIsFloat.MatchString(expr) if isFloat { return &ast.BasicLit{ Kind: token.FLOAT, diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index a35157c2a4f..21e9c2fe646 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -71,6 +71,36 @@ func DefaultConfig() *Config { type Option func(cfg *Config) +// LoadConfig loads the node configuration from disk +func LoadConfig(root string) (*Config, error) { + // Initialize the config as default + var ( + cfg = DefaultConfig() + configPath = filepath.Join(root, defaultConfigPath) + ) + + if !osm.FileExists(configPath) { + return nil, fmt.Errorf("config file at %q does not exist", configPath) + } + + // Load the configuration + loadedCfg, loadErr := LoadConfigFile(configPath) + if loadErr != nil { + return nil, loadErr + } + + // Merge the loaded config with the default values. + // This is done in case the loaded config is missing values + if err := mergo.Merge(loadedCfg, cfg); err != nil { + return nil, err + } + + // Set the root directory + loadedCfg.SetRootDir(root) + + return loadedCfg, nil +} + // LoadOrMakeConfigWithOptions loads the configuration located in the given // root directory, at [defaultConfigFilePath]. // @@ -142,7 +172,7 @@ func TestConfig() *Config { Mempool: mem.TestMempoolConfig(), Consensus: cns.TestConsensusConfig(), TxEventStore: eventstore.DefaultEventStoreConfig(), - Telemetry: telemetry.TestTelemetryConfig(), + Telemetry: telemetry.DefaultTelemetryConfig(), } } @@ -165,11 +195,11 @@ func (cfg *Config) EnsureDirs() error { return fmt.Errorf("no root directory, %w", err) } - if err := osm.EnsureDir(filepath.Join(rootDir, defaultConfigDir), DefaultDirPerm); err != nil { + if err := osm.EnsureDir(filepath.Join(rootDir, DefaultConfigDir), DefaultDirPerm); err != nil { return fmt.Errorf("no config directory, %w", err) } - if err := osm.EnsureDir(filepath.Join(rootDir, defaultSecretsDir), DefaultDirPerm); err != nil { + if err := osm.EnsureDir(filepath.Join(rootDir, DefaultSecretsDir), DefaultDirPerm); err != nil { return fmt.Errorf("no secrets directory, %w", err) } @@ -205,18 +235,18 @@ func (cfg *Config) ValidateBasic() error { var ( DefaultDBDir = "db" - defaultConfigDir = "config" - defaultSecretsDir = "secrets" + DefaultConfigDir = "config" + DefaultSecretsDir = "secrets" - defaultConfigFileName = "config.toml" + DefaultConfigFileName = "config.toml" defaultNodeKeyName = "node_key.json" defaultPrivValKeyName = "priv_validator_key.json" defaultPrivValStateName = "priv_validator_state.json" - defaultConfigPath = filepath.Join(defaultConfigDir, defaultConfigFileName) - defaultPrivValKeyPath = filepath.Join(defaultSecretsDir, defaultPrivValKeyName) - defaultPrivValStatePath = filepath.Join(defaultSecretsDir, defaultPrivValStateName) - defaultNodeKeyPath = filepath.Join(defaultSecretsDir, defaultNodeKeyName) + defaultConfigPath = filepath.Join(DefaultConfigDir, DefaultConfigFileName) + defaultPrivValKeyPath = filepath.Join(DefaultSecretsDir, defaultPrivValKeyName) + defaultPrivValStatePath = filepath.Join(DefaultSecretsDir, defaultPrivValStateName) + defaultNodeKeyPath = filepath.Join(DefaultSecretsDir, defaultNodeKeyName) ) // BaseConfig defines the base configuration for a Tendermint node. diff --git a/tm2/pkg/bft/config/toml.go b/tm2/pkg/bft/config/toml.go index 5d8589394a0..023f9d9e625 100644 --- a/tm2/pkg/bft/config/toml.go +++ b/tm2/pkg/bft/config/toml.go @@ -69,10 +69,10 @@ func ResetTestRoot(testName string) (*Config, string) { } // ensure config and data subdirs are created - if err := osm.EnsureDir(filepath.Join(rootDir, defaultConfigDir), DefaultDirPerm); err != nil { + if err := osm.EnsureDir(filepath.Join(rootDir, DefaultConfigDir), DefaultDirPerm); err != nil { panic(err) } - if err := osm.EnsureDir(filepath.Join(rootDir, defaultSecretsDir), DefaultDirPerm); err != nil { + if err := osm.EnsureDir(filepath.Join(rootDir, DefaultSecretsDir), DefaultDirPerm); err != nil { panic(err) } if err := osm.EnsureDir(filepath.Join(rootDir, DefaultDBDir), DefaultDirPerm); err != nil { diff --git a/tm2/pkg/bft/consensus/state.go b/tm2/pkg/bft/consensus/state.go index 233a2de65d9..3f71dac368c 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -1392,6 +1392,9 @@ func (cs *ConsensusState) finalizeCommit(height int64) { // * cs.Height has been increment to height+1 // * cs.Step is now cstypes.RoundStepNewHeight // * cs.StartTime is set to when we will start round0. + + // Log the telemetry + cs.logTelemetry(block) } // ----------------------------------------------------------------------------- @@ -1766,6 +1769,30 @@ func (cs *ConsensusState) signAddVote(type_ types.SignedMsgType, hash []byte, he return nil } +// logTelemetry logs the consensus state telemetry +func (cs *ConsensusState) logTelemetry(block *types.Block) { + if !telemetry.MetricsEnabled() { + return + } + + // Log the validator telemetry + metrics.ValidatorsCount.Record(context.Background(), int64(cs.Validators.Size())) + metrics.ValidatorsVotingPower.Record(context.Background(), cs.Validators.TotalVotingPower()) + + // Log the block telemetry + if block.Height > 1 { + if lastBlockMeta := cs.blockStore.LoadBlockMeta(block.Height - 1); lastBlockMeta != nil { + metrics.BlockInterval.Record( + context.Background(), + int64(block.Time.Sub(lastBlockMeta.Header.Time).Seconds()), + ) + } + } + + metrics.BlockTxs.Record(context.Background(), block.TotalTxs) + metrics.BlockSizeBytes.Record(context.Background(), int64(block.Size())) +} + // --------------------------------------------------------- func CompareHRS(h1 int64, r1 int, s1 cstypes.RoundStepType, h2 int64, r2 int, s2 cstypes.RoundStepType) int { diff --git a/tm2/pkg/bft/mempool/clist_mempool.go b/tm2/pkg/bft/mempool/clist_mempool.go index 38c24866892..2cad23c68e7 100644 --- a/tm2/pkg/bft/mempool/clist_mempool.go +++ b/tm2/pkg/bft/mempool/clist_mempool.go @@ -3,6 +3,7 @@ package mempool import ( "bytes" "container/list" + "context" "crypto/sha256" "fmt" "log/slog" @@ -19,6 +20,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/log" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/telemetry" + "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" ) // -------------------------------------------------------------------------------- @@ -333,6 +336,22 @@ func (mem *CListMempool) addTx(memTx *mempoolTx) { e := mem.txs.PushBack(memTx) mem.txsMap.Store(txKey(memTx.tx), e) atomic.AddInt64(&mem.txsBytes, int64(len(memTx.tx))) + + // Update the telemetry + mem.logTelemetry() +} + +// logTelemetry logs the mempool telemetry +func (mem *CListMempool) logTelemetry() { + if !telemetry.MetricsEnabled() { + return + } + + // Log the total number of mempool transactions + metrics.NumMempoolTxs.Record(context.Background(), int64(mem.txs.Len())) + + // Log the total number of the mempool cache transactions + metrics.NumCachedTxs.Record(context.Background(), int64(mem.cache.Len())) } // Called from: @@ -347,6 +366,9 @@ func (mem *CListMempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromC if removeFromCache { mem.cache.Remove(tx) } + + // Update the telemetry + mem.logTelemetry() } // callback, which is called after the app checked the tx for the first time. @@ -622,12 +644,13 @@ type txCache interface { Reset() Push(tx types.Tx) bool Remove(tx types.Tx) + Len() int } // mapTxCache maintains a LRU cache of transactions. This only stores the hash // of the tx, due to memory concerns. type mapTxCache struct { - mtx sync.Mutex + mtx sync.RWMutex size int map_ map[[sha256.Size]byte]*list.Element list *list.List @@ -652,6 +675,13 @@ func (cache *mapTxCache) Reset() { cache.mtx.Unlock() } +func (cache *mapTxCache) Len() int { + cache.mtx.RLock() + defer cache.mtx.RUnlock() + + return cache.list.Len() +} + // Push adds the given tx to the cache and returns true. It returns // false if tx is already in the cache. func (cache *mapTxCache) Push(tx types.Tx) bool { @@ -698,6 +728,7 @@ var _ txCache = (*nopTxCache)(nil) func (nopTxCache) Reset() {} func (nopTxCache) Push(types.Tx) bool { return true } func (nopTxCache) Remove(types.Tx) {} +func (nopTxCache) Len() int { return 0 } // -------------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/rpc/config/config.go b/tm2/pkg/bft/rpc/config/config.go index 1428861626c..de576ddb402 100644 --- a/tm2/pkg/bft/rpc/config/config.go +++ b/tm2/pkg/bft/rpc/config/config.go @@ -111,8 +111,8 @@ func DefaultRPCConfig() *RPCConfig { // TestRPCConfig returns a configuration for testing the RPC server func TestRPCConfig() *RPCConfig { cfg := DefaultRPCConfig() - cfg.ListenAddress = "tcp://0.0.0.0:36657" - cfg.GRPCListenAddress = "tcp://0.0.0.0:36658" + cfg.ListenAddress = "tcp://0.0.0.0:26657" + cfg.GRPCListenAddress = "tcp://0.0.0.0:26658" cfg.Unsafe = true return cfg } diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 417f417ba26..88ee26da4a9 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -16,6 +16,8 @@ import ( "strings" "time" + "github.com/gnolang/gno/tm2/pkg/telemetry" + "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" "github.com/gorilla/websocket" "github.com/gnolang/gno/tm2/pkg/amino" @@ -28,6 +30,29 @@ import ( // RegisterRPCFuncs adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. // "result" is the interface on which the result objects are registered, and is populated with every RPCResponse func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc, logger *slog.Logger) { + // Check if metrics are enabled + if telemetry.MetricsEnabled() { + // HTTP endpoints + for funcName, rpcFunc := range funcMap { + mux.HandleFunc( + "/"+funcName, + telemetryMiddleware( + makeHTTPHandler(rpcFunc, logger), + ), + ) + } + + // JSONRPC endpoints + mux.HandleFunc( + "/", + telemetryMiddleware( + handleInvalidJSONRPCPaths(makeJSONRPCHandler(funcMap, logger)), + ), + ) + + return + } + // HTTP endpoints for funcName, rpcFunc := range funcMap { mux.HandleFunc("/"+funcName, makeHTTPHandler(rpcFunc, logger)) @@ -185,6 +210,21 @@ func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc { } } +// telemetryMiddleware is the telemetry middleware handler +func telemetryMiddleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + next.ServeHTTP(w, r) + + // Log the response time + metrics.HTTPRequestTime.Record( + context.Background(), + time.Since(start).Milliseconds(), + ) + } +} + func mapParamsToArgs(rpcFunc *RPCFunc, params map[string]json.RawMessage, argsOffset int) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.argNames)) for i, argName := range rpcFunc.argNames { @@ -258,7 +298,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, raw []byte) ([]reflect.Value, error) { // rpc.http // convert from a function name to the http handler -func makeHTTPHandler(rpcFunc *RPCFunc, logger *slog.Logger) func(http.ResponseWriter, *http.Request) { +func makeHTTPHandler(rpcFunc *RPCFunc, logger *slog.Logger) http.HandlerFunc { // Exception for websocket endpoints if rpcFunc.ws { return func(w http.ResponseWriter, r *http.Request) { @@ -626,6 +666,8 @@ func (wsc *wsConnection) readRoutine() { return wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)) }) + telemetryEnabled := telemetry.MetricsEnabled() + for { select { case <-wsc.Quit(): @@ -647,6 +689,9 @@ func (wsc *wsConnection) readRoutine() { return } + // Log the request response start time + responseStart := time.Now() + // first try to unmarshal the incoming request as an array of RPC requests var ( requests types.RPCRequests @@ -720,6 +765,14 @@ func (wsc *wsConnection) readRoutine() { if len(responses) > 0 { wsc.WriteRPCResponses(responses) + + // Log telemetry + if telemetryEnabled { + metrics.WSRequestTime.Record( + context.Background(), + time.Since(responseStart).Milliseconds(), + ) + } } } } diff --git a/tm2/pkg/crypto/internal/ledger/discover.go b/tm2/pkg/crypto/internal/ledger/discover.go new file mode 100644 index 00000000000..a3402323938 --- /dev/null +++ b/tm2/pkg/crypto/internal/ledger/discover.go @@ -0,0 +1,83 @@ +// Package ledger contains the internals for package crypto/keys/ledger, +// primarily existing so that the Discover function can be mocked elsewhere. +package ledger + +import ( + "github.com/btcsuite/btcd/btcec/v2" + ledger_go "github.com/cosmos/ledger-cosmos-go" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" +) + +// SECP256K1 reflects an interface a Ledger API must implement for SECP256K1 +type SECP256K1 interface { + Close() error + // Returns an uncompressed pubkey + GetPublicKeySECP256K1([]uint32) ([]byte, error) + // Returns a compressed pubkey and bech32 address (requires user confirmation) + GetAddressPubKeySECP256K1([]uint32, string) ([]byte, string, error) + // Signs a message (requires user confirmation) + SignSECP256K1([]uint32, []byte, byte) ([]byte, error) +} + +// Discover defines a function to be invoked at runtime for discovering +// a connected Ledger device. +var Discover DiscoverFn = DiscoverDefault + +// DiscoverDefault is the default function for [Discover]. +func DiscoverDefault() (SECP256K1, error) { + device, err := ledger_go.FindLedgerCosmosUserApp() + if err != nil { + return nil, err + } + + return device, nil +} + +// DiscoverMock can be used as a mock [DiscoverFn]. +func DiscoverMock() (SECP256K1, error) { + privateKey := secp256k1.GenPrivKey() + + _, pubKeyObject := btcec.PrivKeyFromBytes(privateKey[:]) + return discoverMock{ + pubKey: pubKeyObject.SerializeCompressed(), + address: privateKey.PubKey().Address().String(), + }, nil +} + +type discoverMock struct { + MockLedger + pubKey []byte + address string +} + +func (m discoverMock) GetAddressPubKeySECP256K1(data []uint32, str string) ([]byte, string, error) { + return m.pubKey, m.address, nil +} + +// MockLedger is an interface that can be used to create mock [Ledger]. +// Embed it in another type, and implement the method you want to mock: +// +// type MyMock struct { MockLedger } +// func (MyMock) SignSECP256K1(d1, d2 []byte, d3 byte) ([]byte, error) { ... } +type MockLedger struct{} + +func (MockLedger) Close() error { + return nil +} + +func (MockLedger) GetPublicKeySECP256K1(data []uint32) ([]byte, error) { + return nil, nil +} + +func (MockLedger) GetAddressPubKeySECP256K1(data []uint32, str string) ([]byte, string, error) { + return nil, "", nil +} + +func (MockLedger) SignSECP256K1(d1 []uint32, d2 []byte, d3 byte) ([]byte, error) { + return nil, nil +} + +// DiscoverFn defines a Ledger discovery function that returns a +// connected device or an error upon failure. Its allows a method to avoid CGO +// dependencies when Ledger support is potentially not enabled. +type DiscoverFn func() (SECP256K1, error) diff --git a/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go b/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go deleted file mode 100644 index 8a09d060b16..00000000000 --- a/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !ledger_suite -// +build !ledger_suite - -package client - -import "testing" - -func TestAdd_Ledger(t *testing.T) { - t.Skip("Please enable the 'ledger_suite' build tags") -} diff --git a/tm2/pkg/crypto/keys/client/add_ledger_test.go b/tm2/pkg/crypto/keys/client/add_ledger_test.go index c1384efcb79..676c4bff6b4 100644 --- a/tm2/pkg/crypto/keys/client/add_ledger_test.go +++ b/tm2/pkg/crypto/keys/client/add_ledger_test.go @@ -1,6 +1,3 @@ -//go:build ledger_suite -// +build ledger_suite - package client import ( @@ -10,19 +7,17 @@ import ( "time" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/internal/ledger" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// Make sure to run these tests with the following tag enabled: -// -tags='ledger_suite' func TestAdd_Ledger(t *testing.T) { - t.Parallel() + ledger.Discover = ledger.DiscoverMock + t.Cleanup(func() { ledger.Discover = ledger.DiscoverDefault }) t.Run("valid ledger reference added", func(t *testing.T) { - t.Parallel() - var ( kbHome = t.TempDir() baseOptions = BaseOptions{ @@ -63,8 +58,6 @@ func TestAdd_Ledger(t *testing.T) { }) t.Run("valid ledger reference added, overwrite", func(t *testing.T) { - t.Parallel() - var ( kbHome = t.TempDir() baseOptions = BaseOptions{ @@ -116,8 +109,6 @@ func TestAdd_Ledger(t *testing.T) { }) t.Run("valid ledger reference added, no overwrite permission", func(t *testing.T) { - t.Parallel() - var ( kbHome = t.TempDir() baseOptions = BaseOptions{ diff --git a/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go b/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go deleted file mode 100644 index d406f10f2ed..00000000000 --- a/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build !ledger_suite -// +build !ledger_suite - -package keys - -import "testing" - -func TestCreateLedgerUnsupportedAlgo(t *testing.T) { - t.Parallel() - - t.Skip("this test needs to be run with the `ledger_suite` tag enabled") -} - -func TestCreateLedger(t *testing.T) { - t.Parallel() - - t.Skip("this test needs to be run with the `ledger_suite` tag enabled") -} diff --git a/tm2/pkg/crypto/keys/keybase_ledger_test.go b/tm2/pkg/crypto/keys/keybase_ledger_test.go index 0f2fca79f90..ea980cf4f02 100644 --- a/tm2/pkg/crypto/keys/keybase_ledger_test.go +++ b/tm2/pkg/crypto/keys/keybase_ledger_test.go @@ -1,17 +1,16 @@ -//go:build ledger_suite -// +build ledger_suite - package keys import ( "testing" + "github.com/gnolang/gno/tm2/pkg/crypto/internal/ledger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCreateLedgerUnsupportedAlgo(t *testing.T) { - t.Parallel() + ledger.Discover = ledger.DiscoverMock + t.Cleanup(func() { ledger.Discover = ledger.DiscoverDefault }) kb := NewInMemory() _, err := kb.CreateLedger("some_account", Ed25519, "cosmos", 0, 1) @@ -20,7 +19,8 @@ func TestCreateLedgerUnsupportedAlgo(t *testing.T) { } func TestCreateLedger(t *testing.T) { - t.Parallel() + ledger.Discover = ledger.DiscoverMock + t.Cleanup(func() { ledger.Discover = ledger.DiscoverDefault }) kb := NewInMemory() diff --git a/tm2/pkg/crypto/ledger/discover.go b/tm2/pkg/crypto/ledger/discover.go deleted file mode 100644 index d610b56635e..00000000000 --- a/tm2/pkg/crypto/ledger/discover.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ledger_suite -// +build !ledger_suite - -package ledger - -import ( - ledger_go "github.com/cosmos/ledger-cosmos-go" -) - -// discoverLedger defines a function to be invoked at runtime for discovering -// a connected Ledger device. -var discoverLedger discoverLedgerFn = func() (LedgerSECP256K1, error) { - device, err := ledger_go.FindLedgerCosmosUserApp() - if err != nil { - return nil, err - } - - return device, nil -} diff --git a/tm2/pkg/crypto/ledger/discover_mock.go b/tm2/pkg/crypto/ledger/discover_mock.go deleted file mode 100644 index 1f5bdbafdf3..00000000000 --- a/tm2/pkg/crypto/ledger/discover_mock.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build ledger_suite -// +build ledger_suite - -package ledger - -import ( - btcec "github.com/btcsuite/btcd/btcec/v2" - "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" -) - -// discoverLedger defines a function to be invoked at runtime for discovering -// a connected Ledger device. -var discoverLedger discoverLedgerFn = func() (LedgerSECP256K1, error) { - privateKey := secp256k1.GenPrivKey() - - _, pubKeyObject := btcec.PrivKeyFromBytes(privateKey[:]) - - return &MockLedger{ - GetAddressPubKeySECP256K1Fn: func(data []uint32, str string) ([]byte, string, error) { - return pubKeyObject.SerializeCompressed(), privateKey.PubKey().Address().String(), nil - }, - }, nil -} - -type ( - closeDelegate func() error - getPublicKeySECP256K1Delegate func([]uint32) ([]byte, error) - getAddressPubKeySECP256K1Delegate func([]uint32, string) ([]byte, string, error) - signSECP256K1Delegate func([]uint32, []byte, byte) ([]byte, error) -) - -type MockLedger struct { - CloseFn closeDelegate - GetPublicKeySECP256K1Fn getPublicKeySECP256K1Delegate - GetAddressPubKeySECP256K1Fn getAddressPubKeySECP256K1Delegate - SignSECP256K1Fn signSECP256K1Delegate -} - -func (m *MockLedger) Close() error { - if m.CloseFn != nil { - return m.CloseFn() - } - - return nil -} - -func (m *MockLedger) GetPublicKeySECP256K1(data []uint32) ([]byte, error) { - if m.GetPublicKeySECP256K1Fn != nil { - return m.GetPublicKeySECP256K1Fn(data) - } - - return nil, nil -} - -func (m *MockLedger) GetAddressPubKeySECP256K1(data []uint32, str string) ([]byte, string, error) { - if m.GetAddressPubKeySECP256K1Fn != nil { - return m.GetAddressPubKeySECP256K1Fn(data, str) - } - - return nil, "", nil -} - -func (m *MockLedger) SignSECP256K1(d1 []uint32, d2 []byte, d3 byte) ([]byte, error) { - if m.SignSECP256K1Fn != nil { - return m.SignSECP256K1Fn(d1, d2, d3) - } - - return nil, nil -} diff --git a/tm2/pkg/crypto/ledger/ledger_secp256k1.go b/tm2/pkg/crypto/ledger/ledger_secp256k1.go index 56877b813a5..1d0ac8b05f0 100644 --- a/tm2/pkg/crypto/ledger/ledger_secp256k1.go +++ b/tm2/pkg/crypto/ledger/ledger_secp256k1.go @@ -12,27 +12,12 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/internal/ledger" "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" "github.com/gnolang/gno/tm2/pkg/errors" ) type ( - // discoverLedgerFn defines a Ledger discovery function that returns a - // connected device or an error upon failure. Its allows a method to avoid CGO - // dependencies when Ledger support is potentially not enabled. - discoverLedgerFn func() (LedgerSECP256K1, error) - - // LedgerSECP256K1 reflects an interface a Ledger API must implement for SECP256K1 - LedgerSECP256K1 interface { - Close() error - // Returns an uncompressed pubkey - GetPublicKeySECP256K1([]uint32) ([]byte, error) - // Returns a compressed pubkey and bech32 address (requires user confirmation) - GetAddressPubKeySECP256K1([]uint32, string) ([]byte, string, error) - // Signs a message (requires user confirmation) - SignSECP256K1([]uint32, []byte, byte) ([]byte, error) - } - // PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano we // cache the PubKey from the first call to use it later. PrivKeyLedgerSecp256k1 struct { @@ -191,8 +176,8 @@ func convertDERtoBER(signatureDER []byte) ([]byte, error) { return sigBytes, nil } -func getLedgerDevice() (LedgerSECP256K1, error) { - device, err := discoverLedger() +func getLedgerDevice() (ledger.SECP256K1, error) { + device, err := ledger.Discover() if err != nil { return nil, errors.Wrap(err, "ledger nano S") } @@ -200,7 +185,7 @@ func getLedgerDevice() (LedgerSECP256K1, error) { return device, nil } -func validateKey(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1) error { +func validateKey(device ledger.SECP256K1, pkl PrivKeyLedgerSecp256k1) error { pub, err := getPubKeyUnsafe(device, pkl.Path) if err != nil { return err @@ -219,7 +204,7 @@ func validateKey(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1) error { // Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, returning // an error, so this should only trigger if the private key is held in memory // for a while before use. -func sign(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, error) { +func sign(device ledger.SECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, error) { err := validateKey(device, pkl) if err != nil { return nil, err @@ -240,7 +225,7 @@ func sign(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byt // // since this involves IO, it may return an error, which is not exposed // in the PubKey interface, so this function allows better error handling -func getPubKeyUnsafe(device LedgerSECP256K1, path hd.BIP44Params) (crypto.PubKey, error) { +func getPubKeyUnsafe(device ledger.SECP256K1, path hd.BIP44Params) (crypto.PubKey, error) { publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath()) if err != nil { return nil, fmt.Errorf("please open Cosmos app on the Ledger device - error: %w", err) @@ -264,7 +249,7 @@ func getPubKeyUnsafe(device LedgerSECP256K1, path hd.BIP44Params) (crypto.PubKey // // Since this involves IO, it may return an error, which is not exposed // in the PubKey interface, so this function allows better error handling. -func getPubKeyAddrSafe(device LedgerSECP256K1, path hd.BIP44Params, hrp string) (crypto.PubKey, string, error) { +func getPubKeyAddrSafe(device ledger.SECP256K1, path hd.BIP44Params, hrp string) (crypto.PubKey, string, error) { publicKey, addr, err := device.GetAddressPubKeySECP256K1(path.DerivationPath(), hrp) if err != nil { return nil, "", fmt.Errorf("address %s rejected", addr) diff --git a/tm2/pkg/p2p/config/config.go b/tm2/pkg/p2p/config/config.go index a0ed2d39381..48aae35e10a 100644 --- a/tm2/pkg/p2p/config/config.go +++ b/tm2/pkg/p2p/config/config.go @@ -107,7 +107,7 @@ func DefaultP2PConfig() *P2PConfig { // TestP2PConfig returns a configuration for testing the peer-to-peer layer func TestP2PConfig() *P2PConfig { cfg := DefaultP2PConfig() - cfg.ListenAddress = "tcp://0.0.0.0:36656" + cfg.ListenAddress = "tcp://0.0.0.0:26656" cfg.FlushThrottleTimeout = 10 * time.Millisecond cfg.AllowDuplicateIP = true return cfg diff --git a/tm2/pkg/p2p/fuzz.go b/tm2/pkg/p2p/fuzz.go index f41189d409e..03cf88cf750 100644 --- a/tm2/pkg/p2p/fuzz.go +++ b/tm2/pkg/p2p/fuzz.go @@ -21,28 +21,6 @@ type FuzzedConnection struct { config *config.FuzzConnConfig } -// FuzzConn creates a new FuzzedConnection. Fuzzing starts immediately. -func FuzzConn(conn net.Conn) net.Conn { - return FuzzConnFromConfig(conn, config.DefaultFuzzConnConfig()) -} - -// FuzzConnFromConfig creates a new FuzzedConnection from a config. Fuzzing -// starts immediately. -func FuzzConnFromConfig(conn net.Conn, config *config.FuzzConnConfig) net.Conn { - return &FuzzedConnection{ - conn: conn, - start: make(<-chan time.Time), - active: true, - config: config, - } -} - -// FuzzConnAfter creates a new FuzzedConnection. Fuzzing starts when the -// duration elapses. -func FuzzConnAfter(conn net.Conn, d time.Duration) net.Conn { - return FuzzConnAfterFromConfig(conn, d, config.DefaultFuzzConnConfig()) -} - // FuzzConnAfterFromConfig creates a new FuzzedConnection from a config. // Fuzzing starts when the duration elapses. func FuzzConnAfterFromConfig( diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index ac24a2a45b7..cecfc21f3ef 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -717,5 +717,29 @@ func (sw *Switch) addPeer(p Peer) error { sw.Logger.Info("Added peer", "peer", p) + // Update the telemetry data + sw.logTelemetry() + return nil } + +// logTelemetry logs the switch telemetry data +// to global metrics funnels +func (sw *Switch) logTelemetry() { + // Update the telemetry data + if !telemetry.MetricsEnabled() { + return + } + + // Fetch the number of peers + outbound, inbound, dialing := sw.NumPeers() + + // Log the outbound peer count + metrics.OutboundPeers.Record(context.Background(), int64(outbound)) + + // Log the inbound peer count + metrics.InboundPeers.Record(context.Background(), int64(inbound)) + + // Log the dialing peer count + metrics.DialingPeers.Record(context.Background(), int64(dialing)) +} diff --git a/tm2/pkg/p2p/test_util.go b/tm2/pkg/p2p/test_util.go index 3e7abbc6c6f..dd0d9cd6bc7 100644 --- a/tm2/pkg/p2p/test_util.go +++ b/tm2/pkg/p2p/test_util.go @@ -19,24 +19,6 @@ const testCh = 0x01 // ------------------------------------------------ -func AddPeerToSwitchPeerSet(sw *Switch, peer Peer) { - sw.peers.Add(peer) -} - -func CreateRandomPeer(outbound bool) *peer { - addr, netAddr := CreateRoutableAddr() - p := &peer{ - peerConn: peerConn{ - outbound: outbound, - socketAddr: netAddr, - }, - nodeInfo: NodeInfo{NetAddress: netAddr}, - mconn: &conn.MConnection{}, - } - p.SetLogger(log.NewNoopLogger().With("peer", addr)) - return p -} - func CreateRoutableAddr() (addr string, netAddr *NetAddress) { for { id := ed25519.GenPrivKey().PubKey().Address().ID() diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 1801a0af35f..f7c7f5c73b0 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -624,6 +624,8 @@ func (app *BaseApp) getContextForTx(mode RunTxMode, txBytes []byte) (ctx Context // / runMsgs iterates through all the messages and executes them. func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Result) { + ctx = ctx.WithEventLogger(NewEventLogger()) + msgLogs := make([]string, 0, len(msgs)) data := make([]byte, 0, len(msgs)) @@ -641,23 +643,17 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res } var msgResult Result - ctx = ctx.WithEventLogger(NewEventLogger()) // run the message! // skip actual execution for CheckTx mode if mode != RunTxModeCheck { - msgResult = handler.Process(ctx, msg) + msgResult = handler.Process(ctx, msg) // ctx event logger being updated in handler } // Each message result's Data must be length prefixed in order to separate // each result. data = append(data, msgResult.Data...) events = append(events, msgResult.Events...) - defer func() { - events = append(events, ctx.EventLogger().Events()...) - result.Events = events - }() - // TODO append msgevent from ctx. XXX XXX // stop execution and return on first failed message if !msgResult.IsOK() { @@ -672,12 +668,13 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res fmt.Sprintf("msg:%d,success:%v,log:%s,events:%v", i, true, msgResult.Log, events)) } + events = append(events, ctx.EventLogger().Events()...) result.Error = ABCIError(err) result.Data = data + result.Events = events result.Log = strings.Join(msgLogs, "\n") result.GasUsed = ctx.GasMeter().GasConsumed() - result.Events = events return result } diff --git a/tm2/pkg/telemetry/README.md b/tm2/pkg/telemetry/README.md index cadaecc89ab..783b00de789 100644 --- a/tm2/pkg/telemetry/README.md +++ b/tm2/pkg/telemetry/README.md @@ -1,6 +1,7 @@ # Telemetry -The purpose of this package is to provide a way to easily integrate OpenTelemetry Protocol (OTLP) metrics collection into a Tendermint 2 node. +The purpose of this package is to provide a way to easily integrate OpenTelemetry Protocol (OTLP) metrics collection +into a Tendermint 2 node. ## Configure Telemetry @@ -8,10 +9,16 @@ Telemetry can be regularly configured within the TM2 node through the `[telemetry]` section. It is disabled by default. ## OTEL configuration -There are many ways configure the OTEL pipeline for exporting metrics. Here is an example of how a local OTEL collector can be configured to send metrics to Grafana Cloud. This is an optional step and can be highly customized. + +There are many ways configure the OTEL pipeline for exporting metrics. Here is an example of how a local OTEL collector +can be configured to send metrics to Grafana Cloud. This is an optional step and can be highly customized. ### OTEL collector -The latest collector releases can be found [here](https://github.com/open-telemetry/opentelemetry-collector-releases/releases). This is an example of the config that can be used to receive metrics from gno.land and publish them to Grafana Cloud. + +The latest collector releases can be +found [here](https://github.com/open-telemetry/opentelemetry-collector-releases/releases). This is an example of the +config that can be used to receive metrics from gno.land and publish them to Grafana Cloud. + ```yaml receivers: otlp: @@ -29,13 +36,15 @@ exporters: service: pipelines: metrics: - receivers: [otlp] - processors: [batch] - exporters: [otlphttp] + receivers: [ otlp ] + processors: [ batch ] + exporters: [ otlphttp ] ``` -Collector exporter environment variables, including those for authentication, can be found [here](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). +Collector exporter environment variables, including those for authentication, can be +found [here](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). ## Resources + - https://opentelemetry.io/docs/collector/ - https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/setup/collector/ diff --git a/tm2/pkg/telemetry/config/config.go b/tm2/pkg/telemetry/config/config.go index 96c31b48cfd..a5e991fbc89 100644 --- a/tm2/pkg/telemetry/config/config.go +++ b/tm2/pkg/telemetry/config/config.go @@ -1,8 +1,12 @@ -// Package config contains the configuration types and helpers for the telemetry -// package. package config -// Config is the configuration struct for the tm2 telemetry package. +import ( + "errors" +) + +var errEndpointNotSet = errors.New("telemetry exporter endpoint not set") + +// Config is the configuration struct for the tm2 telemetry package type Config struct { MetricsEnabled bool `toml:"enabled"` MeterName string `toml:"meter_name"` @@ -10,7 +14,7 @@ type Config struct { ExporterEndpoint string `toml:"exporter_endpoint" comment:"the endpoint to export metrics to, like a local OpenTelemetry collector"` } -// DefaultTelemetryConfig is the default configuration used for the node. +// DefaultTelemetryConfig is the default configuration used for the node func DefaultTelemetryConfig() *Config { return &Config{ MetricsEnabled: false, @@ -20,8 +24,12 @@ func DefaultTelemetryConfig() *Config { } } -// TestTelemetryConfig is the test configuration. Currently it is an alias for -// [DefaultTelemetryConfig]. -func TestTelemetryConfig() *Config { - return DefaultTelemetryConfig() +// ValidateBasic performs basic telemetry config validation and +// returns an error if any check fails +func (cfg *Config) ValidateBasic() error { + if cfg.ExporterEndpoint == "" { + return errEndpointNotSet + } + + return nil } diff --git a/tm2/pkg/telemetry/config/config_test.go b/tm2/pkg/telemetry/config/config_test.go new file mode 100644 index 00000000000..c61258b886b --- /dev/null +++ b/tm2/pkg/telemetry/config/config_test.go @@ -0,0 +1,29 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_ValidateBasic(t *testing.T) { + t.Parallel() + + t.Run("exporter endpoint not set", func(t *testing.T) { + t.Parallel() + + c := DefaultTelemetryConfig() + c.ExporterEndpoint = "" // empty + + assert.ErrorIs(t, c.ValidateBasic(), errEndpointNotSet) + }) + + t.Run("valid configuration", func(t *testing.T) { + t.Parallel() + + c := DefaultTelemetryConfig() + c.ExporterEndpoint = "0.0.0.0:8080" + + assert.NoError(t, c.ValidateBasic()) + }) +} diff --git a/tm2/pkg/telemetry/exporter/error.go b/tm2/pkg/telemetry/exporter/error.go deleted file mode 100644 index 5df14f0dba3..00000000000 --- a/tm2/pkg/telemetry/exporter/error.go +++ /dev/null @@ -1,5 +0,0 @@ -package exporter - -import "errors" - -var ErrEndpointNotSet = errors.New("telemetry exporter endpoint not set") diff --git a/tm2/pkg/telemetry/init.go b/tm2/pkg/telemetry/init.go index 8d31c6268f8..d0b7a246e8a 100644 --- a/tm2/pkg/telemetry/init.go +++ b/tm2/pkg/telemetry/init.go @@ -4,6 +4,7 @@ package telemetry // https://github.com/open-telemetry/opentelemetry-go/blob/main/example/prometheus/main.go import ( + "fmt" "sync/atomic" "github.com/gnolang/gno/tm2/pkg/telemetry/config" @@ -11,26 +12,34 @@ import ( ) var ( - globalConfig config.Config - globalConfigSet atomic.Bool + globalConfig config.Config + telemetryInitialized atomic.Bool ) -// MetricsEnabled returns true if metrics have been initialized. +// MetricsEnabled returns true if metrics have been initialized func MetricsEnabled() bool { return globalConfig.MetricsEnabled } -// Init sets the configuration for telemetry to c, and if telemetry is enabled, -// starts tracking. -// Init may only be called once. Multiple calls to Init will panic. +// Init initializes the global telemetry func Init(c config.Config) error { - if !globalConfigSet.CompareAndSwap(false, true) { - panic("telemetry configuration has already been set and initialised") + // Check if the metrics are enabled at all + if !c.MetricsEnabled { + return nil } - globalConfig = c - // Initialize metrics to be collected. - if c.MetricsEnabled { - return metrics.Init(c) + + // Validate the configuration + if err := c.ValidateBasic(); err != nil { + return fmt.Errorf("unable to validate config, %w", err) + } + + // Check if it's been enabled already + if !telemetryInitialized.CompareAndSwap(false, true) { + return nil } - return nil + + // Update the global configuration + globalConfig = c + + return metrics.Init(c) } diff --git a/tm2/pkg/telemetry/metrics/init.go b/tm2/pkg/telemetry/metrics/init.go deleted file mode 100644 index ac67a8d71ce..00000000000 --- a/tm2/pkg/telemetry/metrics/init.go +++ /dev/null @@ -1,69 +0,0 @@ -package metrics - -import ( - "context" - - "github.com/gnolang/gno/tm2/pkg/telemetry/config" - "github.com/gnolang/gno/tm2/pkg/telemetry/exporter" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/metric" - sdkMetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" -) - -var ( - // Metrics. - BroadcastTxTimer metric.Int64Histogram - BuildBlockTimer metric.Int64Histogram -) - -func Init(config config.Config) error { - if config.ExporterEndpoint == "" { - return exporter.ErrEndpointNotSet - } - - // Use oltp metric exporter. - exporter, err := otlpmetricgrpc.New( - context.Background(), - otlpmetricgrpc.WithEndpoint(config.ExporterEndpoint), - otlpmetricgrpc.WithInsecure(), // TODO: enable security - ) - if err != nil { - return err - } - - provider := sdkMetric.NewMeterProvider( - // Default period is 1m. - sdkMetric.WithReader(sdkMetric.NewPeriodicReader(exporter)), - sdkMetric.WithResource( - resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String(config.ServiceName), - semconv.ServiceVersionKey.String("1.0.0"), - semconv.ServiceInstanceIDKey.String("gno-node-1"), - ), - ), - ) - otel.SetMeterProvider(provider) - meter := provider.Meter(config.MeterName) - - if BroadcastTxTimer, err = meter.Int64Histogram( - "broadcast_tx_hist", - metric.WithDescription("broadcast tx duration"), - metric.WithUnit("ms"), - ); err != nil { - return err - } - - if BuildBlockTimer, err = meter.Int64Histogram( - "build_block_hist", - metric.WithDescription("block build duration"), - metric.WithUnit("ms"), - ); err != nil { - return err - } - - return nil -} diff --git a/tm2/pkg/telemetry/metrics/metrics.go b/tm2/pkg/telemetry/metrics/metrics.go new file mode 100644 index 00000000000..9d1839fa862 --- /dev/null +++ b/tm2/pkg/telemetry/metrics/metrics.go @@ -0,0 +1,286 @@ +package metrics + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/telemetry/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/metric" + sdkMetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +const ( + broadcastTxTimerKey = "broadcast_tx_hist" + buildBlockTimerKey = "build_block_hist" + + inboundPeersKey = "inbound_peers_hist" + outboundPeersKey = "outbound_peers_hist" + dialingPeersKey = "dialing_peers_hist" + + numMempoolTxsKey = "num_mempool_txs_hist" + numCachedTxsKey = "num_cached_txs_hist" + + vmQueryCallsKey = "vm_query_calls_counter" + vmQueryErrorsKey = "vm_query_errors_counter" + vmGasUsedKey = "vm_gas_used_hist" + vmCPUCyclesKey = "vm_cpu_cycles_hist" + vmExecMsgKey = "vm_exec_msg_hist" + + validatorCountKey = "validator_count_hist" + validatorVotingPowerKey = "validator_vp_hist" + blockIntervalKey = "block_interval_hist" + blockTxsKey = "block_txs_hist" + blockSizeKey = "block_size_hist" + + httpRequestTimeKey = "http_request_time_hist" + wsRequestTimeKey = "ws_request_time_hist" +) + +var ( + // Misc // + + // BroadcastTxTimer measures the transaction broadcast duration + BroadcastTxTimer metric.Int64Histogram + + // Networking // + + // InboundPeers measures the active number of inbound peers + InboundPeers metric.Int64Histogram + + // OutboundPeers measures the active number of outbound peers + OutboundPeers metric.Int64Histogram + + // DialingPeers measures the active number of peers in the dialing state + DialingPeers metric.Int64Histogram + + // Mempool // + + // NumMempoolTxs measures the number of transaction inside the mempool + NumMempoolTxs metric.Int64Histogram + + // NumCachedTxs measures the number of transaction inside the mempool cache + NumCachedTxs metric.Int64Histogram + + // Runtime // + + // VMQueryCalls measures the frequency of VM query calls + VMQueryCalls metric.Int64Counter + + // VMQueryErrors measures the frequency of VM query errors + VMQueryErrors metric.Int64Counter + + // VMGasUsed measures the VM gas usage + VMGasUsed metric.Int64Histogram + + // VMCPUCycles measures the VM CPU cycles + VMCPUCycles metric.Int64Histogram + + // VMExecMsgFrequency measures the frequency of VM operations + VMExecMsgFrequency metric.Int64Counter + + // Consensus // + + // BuildBlockTimer measures the block build duration + BuildBlockTimer metric.Int64Histogram + + // ValidatorsCount measures the size of the active validator set + ValidatorsCount metric.Int64Histogram + + // ValidatorsVotingPower measures the total voting power of the active validator set + ValidatorsVotingPower metric.Int64Histogram + + // BlockInterval measures the interval between 2 subsequent blocks + BlockInterval metric.Int64Histogram + + // BlockTxs measures the number of transactions within the latest block + BlockTxs metric.Int64Histogram + + // BlockSizeBytes measures the size of the latest block in bytes + BlockSizeBytes metric.Int64Histogram + + // RPC // + + // HTTPRequestTime measures the HTTP request response time + HTTPRequestTime metric.Int64Histogram + + // WSRequestTime measures the WS request response time + WSRequestTime metric.Int64Histogram +) + +func Init(config config.Config) error { + // Use oltp metric exporter + exp, err := otlpmetricgrpc.New( + context.Background(), + otlpmetricgrpc.WithEndpoint(config.ExporterEndpoint), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + return fmt.Errorf("unable to create metrics exporter, %w", err) + } + + provider := sdkMetric.NewMeterProvider( + // Default period is 1m + sdkMetric.WithReader(sdkMetric.NewPeriodicReader(exp)), + sdkMetric.WithResource( + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(config.ServiceName), + semconv.ServiceVersionKey.String("1.0.0"), + semconv.ServiceInstanceIDKey.String("gno-node-1"), + ), + ), + ) + otel.SetMeterProvider(provider) + meter := provider.Meter(config.MeterName) + + if BroadcastTxTimer, err = meter.Int64Histogram( + broadcastTxTimerKey, + metric.WithDescription("broadcast tx duration"), + metric.WithUnit("ms"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if BuildBlockTimer, err = meter.Int64Histogram( + buildBlockTimerKey, + metric.WithDescription("block build duration"), + metric.WithUnit("ms"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + // Networking // + if InboundPeers, err = meter.Int64Histogram( + inboundPeersKey, + metric.WithDescription("inbound peer count"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if OutboundPeers, err = meter.Int64Histogram( + outboundPeersKey, + metric.WithDescription("outbound peer count"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if DialingPeers, err = meter.Int64Histogram( + dialingPeersKey, + metric.WithDescription("dialing peer count"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + // Mempool // + if NumMempoolTxs, err = meter.Int64Histogram( + numMempoolTxsKey, + metric.WithDescription("valid mempool transaction count"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if NumCachedTxs, err = meter.Int64Histogram( + numCachedTxsKey, + metric.WithDescription("cached mempool transaction count"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + // Runtime // + if VMQueryCalls, err = meter.Int64Counter( + vmQueryCallsKey, + metric.WithDescription("vm query call frequency"), + ); err != nil { + return fmt.Errorf("unable to create counter, %w", err) + } + + if VMQueryErrors, err = meter.Int64Counter( + vmQueryErrorsKey, + metric.WithDescription("vm query errors call frequency"), + ); err != nil { + return fmt.Errorf("unable to create counter, %w", err) + } + + if VMGasUsed, err = meter.Int64Histogram( + vmGasUsedKey, + metric.WithDescription("VM gas used"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if VMCPUCycles, err = meter.Int64Histogram( + vmCPUCyclesKey, + metric.WithDescription("VM CPU cycles"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if VMExecMsgFrequency, err = meter.Int64Counter( + vmExecMsgKey, + metric.WithDescription("vm msg operation call frequency"), + ); err != nil { + return fmt.Errorf("unable to create counter, %w", err) + } + + // Consensus // + if ValidatorsCount, err = meter.Int64Histogram( + validatorCountKey, + metric.WithDescription("size of the active validator set"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if ValidatorsVotingPower, err = meter.Int64Histogram( + validatorVotingPowerKey, + metric.WithDescription("total voting power of the active validator set"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if BlockInterval, err = meter.Int64Histogram( + blockIntervalKey, + metric.WithDescription("interval between 2 subsequent blocks"), + metric.WithUnit("s"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if BlockTxs, err = meter.Int64Histogram( + blockTxsKey, + metric.WithDescription("number of transactions within the latest block"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if BlockSizeBytes, err = meter.Int64Histogram( + blockSizeKey, + metric.WithDescription("size of the latest block in bytes"), + metric.WithUnit("B"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + // RPC // + + if HTTPRequestTime, err = meter.Int64Histogram( + httpRequestTimeKey, + metric.WithDescription("http request response time"), + metric.WithUnit("ms"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + if WSRequestTime, err = meter.Int64Histogram( + wsRequestTimeKey, + metric.WithDescription("ws request response time"), + metric.WithUnit("ms"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } + + return nil +}